n8n es una plataforma de automatización de workflows de código abierto que conecta APIs, bases de datos y servicios mediante nodos visuales. La instalación por defecto usa SQLite y ejecuta todos los workflows en un solo proceso, algo suficiente para pruebas pero no para cargas reales. En este tutorial desplegarás n8n en Ubuntu 24.04 con Docker Compose, PostgreSQL como base de datos y Nginx con HTTPS, activarás el modo queue con workers que leen de Redis, protegerás los webhooks, configurarás un workflow de errores global y automatizarás las copias de seguridad.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 vCPU y 4 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.
  • Un subdominio, por ejemplo n8n.your_domain, con un registro A apuntando a la IP del servidor.
  • Los puertos 80 y 443 abiertos en el firewall.

Paso 1: Preparar los secretos

n8n cifra todas las credenciales guardadas (tokens de API, contraseñas de bases de datos) con la clave N8N_ENCRYPTION_KEY. Si la clave cambia o se pierde, las credenciales existentes no se pueden descifrar, así que genérala una vez y guárdala en un lugar seguro. Crea el directorio del proyecto:

sudo mkdir -p /opt/n8n
cd /opt/n8n

Genera tres valores aleatorios, uno para la clave de cifrado y dos para las contraseñas de PostgreSQL y Redis:

openssl rand -hex 32
openssl rand -hex 24
openssl rand -hex 24

Crea el archivo .env, que Docker Compose lee automáticamente para sustituir las variables:

sudo nano /opt/n8n/.env
N8N_DOMAIN=n8n.your_domain
N8N_ENCRYPTION_KEY=your_64_char_encryption_key
POSTGRES_PASSWORD=your_postgres_password
REDIS_PASSWORD=your_redis_password
GENERIC_TIMEZONE=Europe/Madrid

Sustituye los valores por los que acabas de generar y restringe los permisos del archivo:

sudo chmod 600 /opt/n8n/.env

Paso 2: Crear el archivo de Docker Compose

Este archivo define cuatro servicios: PostgreSQL, Redis, el proceso principal de n8n (interfaz, API, webhooks y disparadores) y los workers que ejecutan los workflows. Las variables comunes a n8n y a los workers se declaran una sola vez con un ancla YAML (x-n8n-env):

sudo nano /opt/n8n/compose.yaml
x-n8n-env: &n8n-env
  DB_TYPE: postgresdb
  DB_POSTGRESDB_HOST: postgres
  DB_POSTGRESDB_PORT: 5432
  DB_POSTGRESDB_DATABASE: n8n
  DB_POSTGRESDB_USER: n8n
  DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
  N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
  EXECUTIONS_MODE: queue
  QUEUE_BULL_REDIS_HOST: redis
  QUEUE_BULL_REDIS_PORT: 6379
  QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
  QUEUE_HEALTH_CHECK_ACTIVE: "true"
  GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
  TZ: ${GENERIC_TIMEZONE}
  EXECUTIONS_DATA_PRUNE: "true"
  EXECUTIONS_DATA_MAX_AGE: 336
  EXECUTIONS_DATA_SAVE_ON_SUCCESS: none

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: n8n
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep PONG"]
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    restart: unless-stopped

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    environment:
      <<: *n8n-env
      N8N_HOST: ${N8N_DOMAIN}
      N8N_PROTOCOL: https
      N8N_PORT: 5678
      WEBHOOK_URL: https://${N8N_DOMAIN}/
      N8N_PROXY_HOPS: 1
      OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true"
    ports:
      - "127.0.0.1:5678:5678"
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  n8n-worker:
    image: docker.n8n.io/n8nio/n8n:latest
    command: worker --concurrency=10
    environment:
      <<: *n8n-env
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - n8n
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  n8n_data:

Puntos clave de esta configuración:

  • EXECUTIONS_MODE: queue hace que el proceso principal no ejecute workflows: los encola en Redis y los workers los recogen. Así puedes añadir workers cuando crezca la carga.
  • OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS envía también a los workers las ejecuciones que lanzas desde el editor.
  • EXECUTIONS_DATA_PRUNE y EXECUTIONS_DATA_MAX_AGE (en horas, 336 son 14 días) evitan que la tabla de ejecuciones crezca sin límite. Con EXECUTIONS_DATA_SAVE_ON_SUCCESS: none solo se guardan las ejecuciones fallidas, que son las que necesitas revisar.
  • N8N_PROXY_HOPS: 1 indica a n8n que hay un proxy delante, para que registre la IP real del cliente.
  • El puerto 5678 solo escucha en 127.0.0.1; el acceso público pasa por Nginx.

Arranca la pila:

sudo docker compose up -d

Comprueba que los cuatro contenedores están en marcha y que PostgreSQL y Redis aparecen como healthy:

sudo docker compose ps
NAME                  IMAGE                            STATUS
n8n-n8n-1             docker.n8n.io/n8nio/n8n:latest   Up 30 seconds
n8n-n8n-worker-1      docker.n8n.io/n8nio/n8n:latest   Up 29 seconds
n8n-postgres-1        postgres:16                      Up 41 seconds (healthy)
n8n-redis-1           redis:7                          Up 41 seconds (healthy)

Revisa que el worker se ha conectado a la cola:

sudo docker compose logs n8n-worker --tail 20

La salida debe indicar que el worker está listo y escuchando trabajos, sin errores de conexión a Redis o PostgreSQL. Comprueba también el endpoint de salud del proceso principal:

curl -s http://127.0.0.1:5678/healthz
{"status":"ok"}

Paso 3: Publicar n8n con Nginx y HTTPS

Instala Nginx y Certbot:

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

Crea el bloque de servidor. n8n usa WebSockets para actualizar el editor en tiempo real, por eso se pasan las cabeceras Upgrade y Connection. proxy_buffering off evita retrasos en las respuestas en streaming:

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

    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_buffering off;
        proxy_read_timeout 3600s;
    }
}

Activa el sitio, valida la configuración y solicita el certificado:

sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d n8n.your_domain

Abre https://n8n.your_domain. La primera vez n8n te pedirá crear la cuenta de propietario (owner) con tu email y una contraseña. Hazlo de inmediato: hasta entonces, cualquiera que llegue a la URL podría reclamar la instancia. Después, en Settings > Personal, activa la autenticación en dos pasos.

Paso 4: Recibir webhooks autenticados

Un nodo Webhook expone una URL que dispara el workflow. n8n genera dos URLs para cada webhook:

  • https://n8n.your_domain/webhook-test/<ruta>: solo funciona mientras el editor está escuchando (Listen for test event). Sirve para desarrollar.
  • https://n8n.your_domain/webhook/<ruta>: la URL de producción, que funciona cuando el workflow está activado (o publicado, según la versión de n8n).

Cualquiera que conozca la URL de producción puede llamarla, así que conviene exigir un secreto. Crea un workflow nuevo, añade un nodo Webhook y configúralo así:

  1. HTTP Method: POST.
  2. Path: pedidos.
  3. Authentication: Header Auth. Crea una credencial nueva con Name X-Webhook-Secret y como Value un secreto largo (por ejemplo, la salida de openssl rand -hex 24).
  4. Respond: Immediately, para que el cliente reciba un 200 sin esperar a que termine el workflow.

Añade detrás cualquier nodo de prueba (por ejemplo, No Operation, do nothing), guarda y activa el workflow. Envía una petición con el secreto:

curl -i -X POST https://n8n.your_domain/webhook/pedidos \
  -H "X-Webhook-Secret: your_webhook_secret" \
  -H "Content-Type: application/json" \
  -d '{"pedido_id": 1001, "total": 150}'
HTTP/1.1 200 OK
...
{"message":"Workflow was started"}

Repite la petición sin la cabecera: n8n debe responder 403 Forbidden. En la pestaña Executions del workflow verás la ejecución con los datos recibidos. Como está en modo queue, la ha procesado el worker, no el proceso principal.

Paso 5: Centralizar la gestión de errores

Cuando un workflow activo falla, n8n puede lanzar automáticamente otro workflow que te avise. Crea un workflow llamado Errores globales y añade como disparador el nodo Error Trigger. Detrás, añade el nodo de notificación que uses (Slack, Telegram, email). En el texto del mensaje puedes usar expresiones con los datos del fallo:

Fallo en el workflow {{ $json.workflow.name }}
Nodo: {{ $json.execution.lastNodeExecuted }}
Error: {{ $json.execution.error.message }}
Ejecución: {{ $json.execution.url }}

Guarda el workflow. Después, en cada workflow de producción, abre Settings (menú de los tres puntos) y en Error workflow selecciona Errores globales.

Para comprobarlo, añade temporalmente a un workflow de prueba un nodo Stop and Error con un mensaje, actívalo y dispáralo: debes recibir la notificación con el nombre del workflow y el mensaje de error.

Si prefieres tratar un fallo dentro del propio workflow en lugar de detenerlo, abre la configuración de un nodo (pestaña Settings) y cambia On Error a Continue (using error output). El nodo tendrá una segunda salida por la que salen solo los elementos que fallaron, y ahí puedes conectar un reintento o un aviso.

Paso 6: Reutilizar lógica con sub-workflows

Si varios workflows repiten los mismos pasos (por ejemplo, enviar una notificación con un formato concreto), muévelos a un sub-workflow:

  1. Crea un workflow nuevo que empiece por el disparador When Executed by Another Workflow (en versiones anteriores, Execute Workflow Trigger). Define los campos de entrada que espera, por ejemplo destinatario y mensaje.
  2. Añade los nodos que hacen el trabajo y guarda.
  3. En el workflow principal, añade el nodo Execute Workflow, elige el sub-workflow en From list y rellena los campos de entrada con expresiones, como {{ $json.email }}.

Los datos que devuelva el último nodo del sub-workflow vuelven al workflow principal. Ejecuta el principal desde el editor y comprueba en Executions que aparecen ambas ejecuciones enlazadas.

Paso 7: Escalar los workers

Cada worker procesa hasta 10 ejecuciones simultáneas (--concurrency=10). Para aumentar la capacidad, añade más réplicas del servicio de workers:

cd /opt/n8n
sudo docker compose up -d --scale n8n-worker=3
sudo docker compose ps n8n-worker
NAME               IMAGE                            STATUS
n8n-n8n-worker-1   docker.n8n.io/n8nio/n8n:latest   Up 12 minutes
n8n-n8n-worker-2   docker.n8n.io/n8nio/n8n:latest   Up 8 seconds
n8n-n8n-worker-3   docker.n8n.io/n8nio/n8n:latest   Up 8 seconds

Vigila el consumo con sudo docker stats antes de añadir más: cada worker necesita memoria propia, y los workflows que manejan archivos grandes pueden consumir cientos de MB por ejecución. Para que el número de réplicas se mantenga tras un docker compose up -d posterior, añade deploy: { replicas: 3 } al servicio n8n-worker en compose.yaml.

Paso 8: Hacer copias de seguridad

Necesitas dos cosas para recuperar una instancia: la base de datos de PostgreSQL y la clave N8N_ENCRYPTION_KEY del archivo .env. Crea un script que vuelque la base de datos a diario:

sudo nano /usr/local/bin/n8n-backup
#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR=/var/backups/n8n
mkdir -p "$BACKUP_DIR"
cd /opt/n8n

docker compose exec -T postgres pg_dump -U n8n -d n8n --format=custom > "$BACKUP_DIR/n8n-$(date +%F).dump"
find "$BACKUP_DIR" -name 'n8n-*.dump' -mtime +14 -delete

Hazlo ejecutable, pruébalo y comprueba que se ha creado el volcado:

sudo chmod 750 /usr/local/bin/n8n-backup
sudo /usr/local/bin/n8n-backup
ls -lh /var/backups/n8n

Prográmalo con cron para que se ejecute cada noche a las 02:30:

echo '30 2 * * * root /usr/local/bin/n8n-backup' | sudo tee /etc/cron.d/n8n-backup

Copia /var/backups/n8n y el archivo /opt/n8n/.env a un almacenamiento fuera del servidor. Además, puedes exportar los workflows como JSON, útil para llevarlos a control de versiones:

sudo docker compose exec n8n n8n export:workflow --all --separate --output=/home/node/.n8n/export/

Paso 9: Actualizar n8n

Antes de actualizar, lee las notas de la versión en el changelog de n8n, sobre todo si cambia la versión principal. Haz una copia con el script anterior y actualiza las imágenes:

cd /opt/n8n
sudo /usr/local/bin/n8n-backup
sudo docker compose pull
sudo docker compose up -d

n8n aplica automáticamente las migraciones de la base de datos al arrancar. Revisa los logs con sudo docker compose logs n8n --tail 50 para confirmar que han terminado sin errores. En producción es preferible fijar una versión concreta de la imagen (la etiqueta con el número de versión, como docker.n8n.io/n8nio/n8n:<versión>) en lugar de latest, para controlar cuándo se actualiza.

Solución de problemas

Las URLs de webhook que muestra el editor usan http://localhost:5678. Falta WEBHOOK_URL o está mal escrita. Corrígela en compose.yaml y recrea el contenedor con sudo docker compose up -d.

El webhook de producción devuelve 404. El workflow no está activo, o estás llamando a /webhook-test/ en lugar de /webhook/.

Las ejecuciones se quedan en espera y nunca terminan. Ningún worker está leyendo la cola. Comprueba con sudo docker compose logs n8n-worker que los workers arrancan y que usan la misma contraseña de Redis y la misma N8N_ENCRYPTION_KEY que el proceso principal.

Credentials could not be decrypted. La N8N_ENCRYPTION_KEY actual no es la que se usó para guardar las credenciales. Restaura el valor original desde tu copia del archivo .env.

El editor se desconecta o no muestra el progreso de las ejecuciones. Nginx no está pasando las cabeceras de WebSocket. Revisa que el bloque de servidor incluye Upgrade y Connection y recarga Nginx.

Conclusión

Tienes n8n funcionando en modo queue con PostgreSQL, Redis y workers escalables, publicado con HTTPS, con webhooks autenticados, un workflow de errores global y copias de seguridad diarias. Como siguientes pasos, puedes crear una clave en Settings > n8n API para gestionar workflows desde scripts, mover los workflows exportados a un repositorio Git y añadir procesos dedicados de webhooks si recibes un volumen alto de peticiones entrantes.