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"}
Importantela API de Fn no tiene autenticación y el puerto 8080 queda publicado en todas las interfaces. Docker gestiona sus propias reglas de iptables, así que UFW no lo bloquea. Bloquea el puerto 8080 en el firewall de red de tu proveedor (en CubePath, desde el panel) y expón solo los triggers HTTP a través de Nginx, como verás en el paso 8.
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.pycontiene el código de la función.func.yamldescribe la función: nombre, versión, runtime, imágenes de build y de ejecución, memoria y triggers.requirements.txtlista 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/pedidodel 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.
