Fn Project es una plataforma serverless de código abierto en la que cada función es una imagen de contenedor. El servidor Fn recibe las invocaciones, arranca el contenedor de la función y lo mantiene caliente unos segundos para atender las siguientes peticiones sin coste de arranque. En este tutorial instalarás la CLI y el servidor Fn en Ubuntu 24.04, desplegarás una función Python, la publicarás con un trigger HTTP detrás de Nginx con HTTPS y configurarás el servidor para que arranque con el sistema.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS de 64 bits, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Al menos 2 GB de RAM (4 GB si vas a construir funciones en Java).
  • Docker Engine instalado desde el repositorio oficial, con tu usuario en el grupo docker.
  • Para la parte de HTTPS: un dominio, por ejemplo fn.your_domain, con un registro DNS A apuntando a la IP del servidor, y los puertos 80 y 443 abiertos.

Paso 1: Comprobar Docker

El servidor Fn se ejecuta como contenedor y construye las funciones con Docker, así que tu usuario debe poder usarlo sin sudo:

docker run --rm hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

Si recibes permission denied, ejecuta sudo usermod -aG docker "$USER", cierra la sesión y vuelve a entrar.

Paso 2: Instalar la CLI de Fn

El proyecto distribuye la CLI con un script de instalación que detecta tu sistema, descarga el binario fn desde GitHub y lo copia en /usr/local/bin. Descárgalo primero para poder revisarlo:

curl -fsSL https://raw.githubusercontent.com/fnproject/cli/master/install -o fn-install.sh
less fn-install.sh

Cuando hayas comprobado lo que hace, ejecútalo con sudo (necesita escribir en /usr/local/bin) y borra el script:

sudo sh fn-install.sh
rm fn-install.sh

Verifica la instalación:

fn version
Client version is latest version: 0.6.x
Server version:  ?

El servidor aparece como ? porque todavía no está en marcha.

Paso 3: Arrancar el servidor Fn

fn start descarga la imagen fnproject/fnserver y la ejecuta con el socket de Docker montado. Guarda la base de datos del servidor en ~/.fn/data, por lo que las aplicaciones y funciones persisten entre reinicios. Arráncalo en segundo plano:

fn start -d

Comprueba que el contenedor está en marcha y que la API responde en el puerto 8080:

docker ps --filter name=fnserver
curl -s http://127.0.0.1:8080/version
CONTAINER ID   IMAGE               STATUS         PORTS                    NAMES
b71c0e5a9d12   fnproject/fnserver  Up 10 seconds  0.0.0.0:8080->8080/tcp   fnserver
{"version":"0.3.7xx"}

Paso 4: Crear una aplicación y una función Python

En Fn, las funciones se agrupan en aplicaciones, que comparten configuración y triggers bajo un mismo prefijo. Crea la aplicación:

fn create app demo
fn list apps
Successfully created app:  demo
NAME   ID
demo   01J...

Genera el esqueleto de una función Python con fn init:

mkdir -p ~/fn && cd ~/fn
fn init --runtime python saludo
cd saludo
ls
func.py  func.yaml  requirements.txt
  • func.py contiene el código de la función.
  • func.yaml describe la función: nombre, versión, runtime, imágenes de build y de ejecución, memoria y triggers.
  • requirements.txt lista las dependencias de pip; incluye el FDK de Python (fdk), que gestiona la comunicación con el servidor.

Sustituye el contenido de func.py por esta función, que calcula el total de un pedido a partir de un JSON:

nano func.py
import io
import json
import logging

from fdk import response

PRECIOS = {"vps": 19.99, "baremetal": 199.99}


def handler(ctx, data: io.BytesIO = None):
    try:
        pedido = json.loads(data.getvalue())
    except (ValueError, AttributeError):
        pedido = {}

    producto = pedido.get("producto", "vps")
    cantidad = int(pedido.get("cantidad", 1))
    logging.getLogger().info("Pedido recibido: %s x %d", producto, cantidad)

    resultado = {
        "producto": producto,
        "cantidad": cantidad,
        "total": round(PRECIOS.get(producto, 0) * cantidad, 2),
    }
    return response.Response(
        ctx,
        response_data=json.dumps(resultado),
        headers={"Content-Type": "application/json"},
    )

Paso 5: Ajustar límites y añadir un trigger HTTP

Abre func.yaml. Conserva las líneas que ha generado fn init (schema_version, name, version, runtime, build_image, run_image y entrypoint) y añade o ajusta al final los límites y el trigger:

nano func.yaml
memory: 256
timeout: 30
idle_timeout: 60
triggers:
- name: saludo
  type: http
  source: /pedido
  • memory: memoria máxima del contenedor de la función, en MB.
  • timeout: tiempo máximo de una invocación, en segundos.
  • idle_timeout: segundos que el contenedor permanece caliente tras la última invocación. Mientras siga vivo, las nuevas peticiones se atienden sin cold start.
  • triggers: publica la función en la ruta /t/demo/pedido del servidor.

Paso 6: Desplegar la función

fn deploy construye la imagen de la función, incrementa la versión en func.yaml y la registra en el servidor. Con --local la imagen se queda en el Docker local en vez de subirse a un registro, que es lo adecuado con un solo servidor:

fn deploy --app demo --local
Deploying saludo to app: demo
Bumped to version 0.0.2
Building image saludo:0.0.2 ...
Updating function saludo using image saludo:0.0.2...
Successfully created function: saludo with saludo:0.0.2
Successfully created trigger: saludo
Trigger Endpoint: http://localhost:8080/t/demo/pedido

El primer despliegue tarda más porque descarga las imágenes base de Python. Comprueba que la función y el trigger están registrados:

fn list functions demo
fn list triggers demo

Paso 7: Invocar la función

Invoca la función con la CLI, pasando el JSON por la entrada estándar:

echo '{"producto": "vps", "cantidad": 3}' | fn invoke demo saludo --content-type application/json
{"producto": "vps", "cantidad": 3, "total": 59.97}

Y ahora a través del trigger HTTP, que es como la llamarán otras aplicaciones:

curl -s -X POST http://127.0.0.1:8080/t/demo/pedido \
  -H 'Content-Type: application/json' \
  -d '{"producto": "baremetal", "cantidad": 2}'
{"producto": "baremetal", "cantidad": 2, "total": 399.98}

Si repites la petición en pocos segundos, notarás que responde más rápido: el contenedor sigue caliente. Puedes verlo con docker ps, donde aparece un contenedor de la función mientras dura el idle_timeout.

Para pasar configuración a la función sin tocar el código, usa variables de configuración, que llegan como variables de entorno:

fn config function demo saludo MONEDA EUR
fn list config function demo saludo

Paso 8: Publicar los triggers con Nginx y HTTPS

Nginx actuará como proxy inverso: servirá HTTPS en tu dominio y reenviará solo las rutas /t/, que son las de los triggers. La API de gestión (/v2/) no se publica. Instala Nginx y Certbot:

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ufw allow 'Nginx Full'

Crea el bloque de servidor, sustituyendo fn.your_domain por tu dominio:

sudo nano /etc/nginx/sites-available/fn
server {
    listen 80;
    listen [::]:80;
    server_name fn.your_domain;

    location /t/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }

    location / {
        return 404;
    }
}

Activa el sitio, valida la configuración y recarga Nginx:

sudo ln -s /etc/nginx/sites-available/fn /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Obtén un certificado de Let's Encrypt. Certbot modificará el bloque para escuchar en el 443 y redirigir HTTP a HTTPS:

sudo certbot --nginx -d fn.your_domain

Prueba la función desde tu equipo:

curl -s -X POST https://fn.your_domain/t/demo/pedido \
  -H 'Content-Type: application/json' \
  -d '{"producto": "vps", "cantidad": 1}'
{"producto": "vps", "cantidad": 1, "total": 19.99}

Una petición a https://fn.your_domain/v2/apps debe devolver 404, lo que confirma que la API de gestión no está expuesta.

Paso 9: Arrancar el servidor Fn con systemd

El contenedor que lanza fn start se elimina al detenerse y no vuelve a arrancar tras un reinicio del servidor. Crea una unidad de systemd que ejecute fn start en primer plano con tu usuario. Primero, detén el servidor actual:

fn stop

Crea la unidad, sustituyendo your_user por tu usuario (debe pertenecer al grupo docker):

sudo nano /etc/systemd/system/fnserver.service
[Unit]
Description=Fn Project server
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
User=your_user
ExecStart=/usr/local/bin/fn start
ExecStop=/usr/local/bin/fn stop
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Recarga systemd, habilita el servicio y arráncalo:

sudo systemctl daemon-reload
sudo systemctl enable --now fnserver
systemctl status fnserver --no-pager
● fnserver.service - Fn Project server
     Loaded: loaded (/etc/systemd/system/fnserver.service; enabled; preset: enabled)
     Active: active (running) since ...

Comprueba que la aplicación sigue ahí, ya que los datos están en ~/.fn/data:

fn list functions demo

Los logs del servidor se consultan con journalctl -u fnserver -f.

Solución de problemas

fn deploy falla con Cannot connect to the Docker daemon. Tu usuario no está en el grupo docker o no has vuelto a iniciar sesión. Comprueba con groups.

fn start falla porque el puerto 8080 está en uso. Localiza el proceso con sudo ss -tlnp | grep ':8080' y detenlo, o cambia el puerto del otro servicio.

La invocación devuelve un error de timeout. Aumenta timeout en func.yaml, vuelve a desplegar con fn deploy --app demo --local y revisa los logs de la función. Por defecto, Fn envía la salida de las funciones a los logs del servidor, visibles con journalctl -u fnserver o docker logs fnserver.

Nginx devuelve 502 Bad Gateway. El servidor Fn no está en marcha. Revisa systemctl status fnserver y curl -s http://127.0.0.1:8080/version.

Conclusión

Tienes Fn Project funcionando en Ubuntu 24.04 con una función Python desplegada, accesible mediante un trigger HTTP publicado por Nginx con HTTPS y un servidor que arranca con el sistema. Como siguientes pasos, puedes crear funciones en Go, Node.js o Java con fn init --runtime, usar fn config app para compartir configuración entre funciones de una aplicación, o subir tus imágenes a un registro privado para desplegar las mismas funciones en varios servidores.