n8n es una plataforma de automatización de flujos de trabajo con editor visual y cientos de integraciones (Slack, Gmail, Google Sheets, bases de datos, APIs HTTP), que puedes alojar en tu propio servidor como alternativa a Zapier o Make. En este tutorial desplegarás n8n con Docker Compose y PostgreSQL en Ubuntu 24.04, lo publicarás detrás de Nginx con un certificado de Let's Encrypt, crearás un flujo que responde a un webhook y programarás copias de seguridad diarias.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM.
- Un usuario no root con privilegios
sudo. - Docker Engine y el plugin Docker Compose instalados desde el repositorio oficial de Docker.
- Un subdominio (en esta guía,
n8n.your_domain) con un registro DNS A que apunte a la IP pública del servidor. - Los puertos 80 y 443 accesibles desde Internet.
Comprueba que Docker y Compose responden:
docker --version
docker compose version
Paso 1: Crear el directorio y los secretos
Crea el directorio del proyecto:
sudo mkdir -p /opt/n8n
cd /opt/n8n
n8n necesita dos secretos: la contraseña de PostgreSQL y una clave de cifrado (N8N_ENCRYPTION_KEY) con la que cifra las credenciales que guardes en los flujos. Si pierdes esa clave, las credenciales almacenadas dejan de poder descifrarse, así que guárdala también fuera del servidor.
Crea el archivo .env con valores aleatorios. Docker Compose lo lee automáticamente desde el directorio del proyecto:
sudo tee /opt/n8n/.env > /dev/null <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
N8N_DOMAIN=n8n.your_domain
TIMEZONE=Europe/Madrid
EOF
sudo chmod 600 /opt/n8n/.env
Sustituye n8n.your_domain por tu subdominio y ajusta TIMEZONE a tu zona horaria. La zona horaria afecta a los nodos programados (Schedule Trigger).
Paso 2: Crear el archivo de Docker Compose
Crea el archivo de Compose:
sudo nano /opt/n8n/compose.yaml
Añade el siguiente contenido:
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
environment:
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}
N8N_HOST: ${N8N_DOMAIN}
N8N_PORT: 5678
N8N_PROTOCOL: https
WEBHOOK_URL: https://${N8N_DOMAIN}/
N8N_PROXY_HOPS: 1
GENERIC_TIMEZONE: ${TIMEZONE}
TZ: ${TIMEZONE}
NODE_ENV: production
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:
Qué hace cada ajuste relevante:
DB_TYPE: postgresdbsustituye la base de datos SQLite por defecto por PostgreSQL, más adecuada para producción.N8N_PROTOCOL,N8N_HOSTyWEBHOOK_URLindican a n8n su URL pública. SinWEBHOOK_URL, el editor mostraría las URL de los webhooks conhttp://localhost:5678.N8N_PROXY_HOPS: 1le dice a n8n que hay un proxy inverso delante, para que lea correctamente la IP del cliente deX-Forwarded-For.- El puerto se publica solo en
127.0.0.1. Docker gestiona sus propias reglas de iptables y un puerto publicado en todas las interfaces quedaría accesible aunque UFW lo bloquee.
Paso 3: Arrancar n8n
Arranca los contenedores:
cd /opt/n8n
sudo docker compose up -d
Comprueba su estado:
sudo docker compose ps
NAME IMAGE SERVICE STATUS PORTS
n8n-n8n-1 docker.n8n.io/n8nio/n8n:latest n8n Up 30 seconds 127.0.0.1:5678->5678/tcp
n8n-postgres-1 postgres:16 postgres Up 41 seconds (healthy) 5432/tcp
n8n expone el endpoint /healthz. Consúltalo desde el servidor:
curl -s http://127.0.0.1:5678/healthz
{"status":"ok"}
Si no responde, revisa los registros con sudo docker compose logs n8n. Un error de conexión a la base de datos suele indicar que el archivo .env no está en /opt/n8n o que cambiaste la contraseña después de crear el volumen de PostgreSQL.
Paso 4: Configurar Nginx y HTTPS
Instala Nginx y Certbot:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Crea el bloque de servidor:
sudo nano /etc/nginx/sites-available/n8n
Añade esta configuración. El editor de n8n recibe actualizaciones en tiempo real por WebSocket, por eso se reenvían las cabeceras Upgrade y Connection; proxy_buffering off evita retrasos en esas respuestas y el proxy_read_timeout alto da margen a los webhooks que tardan en responder:
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_buffering off;
proxy_read_timeout 300s;
}
}
Activa el sitio y recarga Nginx:
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Si usas UFW, permite SSH y el tráfico web:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Obtén el certificado. Certbot añade la configuración TLS y la redirección a HTTPS:
sudo certbot --nginx -d n8n.your_domain
Comprueba que la renovación automática funcionará:
sudo certbot renew --dry-run
Paso 5: Crear la cuenta de propietario
Abre https://n8n.your_domain. En el primer acceso, n8n pide crear la cuenta de propietario (owner): introduce tu correo, nombre y una contraseña robusta. Esta cuenta administra la instancia; a otros usuarios los invitarás después desde Settings y luego Users.
Consejohasta que crees la cuenta de propietario, cualquiera que abra la URL puede hacerlo. Completa este paso justo después de obtener el certificado.
Paso 6: Crear un flujo con webhook
Para comprobar que la instalación funciona de extremo a extremo, crea un flujo que recibe una petición HTTP y devuelve una respuesta:
- Pulsa Create Workflow (o Add workflow) y ponle el nombre
Prueba webhook. - Añade el nodo disparador Webhook. En HTTP Method elige
POSTy en Path escribeprueba. - Guarda el flujo.
- Pulsa Listen for test event en el nodo Webhook. n8n mostrará la URL de prueba, que contiene
/webhook-test/prueba.
Desde tu equipo, envía una petición a esa URL:
curl -X POST https://n8n.your_domain/webhook-test/prueba \
-H 'Content-Type: application/json' \
-d '{"nombre": "CubePath"}'
{"message":"Workflow was started"}
En el editor verás los datos recibidos en la salida del nodo Webhook. Las URL de prueba solo funcionan mientras el editor está escuchando. Para que el flujo atienda peticiones de forma permanente, actívalo (o publícalo, según tu versión de n8n) con el control de la parte superior del editor. A partir de ahí responde en la URL de producción:
curl -X POST https://n8n.your_domain/webhook/prueba \
-H 'Content-Type: application/json' \
-d '{"nombre": "CubePath"}'
Cada ejecución queda registrada en la pestaña Executions del flujo. Si la URL que muestra el nodo empieza por http://localhost:5678, revisa la variable WEBHOOK_URL del paso 2.
Las credenciales de servicios externos (tokens de Slack, cuentas de Google, claves de API) se crean desde Credentials o directamente desde el nodo que las necesita, y se guardan cifradas con N8N_ENCRYPTION_KEY.
Paso 7: Programar copias de seguridad
Una copia completa de n8n consta del volcado de PostgreSQL (flujos, credenciales cifradas, historial de ejecuciones) y del archivo .env, que contiene la clave de cifrado. Crea el script:
sudo nano /usr/local/bin/n8n-backup
Añade el siguiente contenido:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="/opt/n8n"
BACKUP_DIR="/var/backups/n8n"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
cd "$PROJECT_DIR"
docker compose exec -T postgres pg_dump -U n8n -d n8n \
| gzip > "$BACKUP_DIR/n8n-db-$STAMP.sql.gz"
cp .env "$BACKUP_DIR/n8n-env-$STAMP"
chmod 600 "$BACKUP_DIR/n8n-env-$STAMP"
# Conserva las copias de los últimos 14 días
find "$BACKUP_DIR" -type f -name 'n8n-*' -mtime +14 -delete
Hazlo ejecutable, ejecútalo una vez y comprueba el resultado:
sudo chmod 750 /usr/local/bin/n8n-backup
sudo /usr/local/bin/n8n-backup
sudo ls -lh /var/backups/n8n
-rw------- 1 root root 71 sep 25 03:30 n8n-env-20260925-033000
-rw-r--r-- 1 root root 48K sep 25 03:30 n8n-db-20260925-033000.sql.gz
Prográmalo cada noche a las 03:30:
echo '30 3 * * * root /usr/local/bin/n8n-backup' | sudo tee /etc/cron.d/n8n-backup
Copia regularmente /var/backups/n8n a otro servidor o a almacenamiento de objetos: una copia en el mismo disco no te protege de un fallo del servidor.
Paso 8: Actualizar n8n
n8n publica versiones cada semana. Haz una copia de seguridad, descarga la imagen nueva y recrea el contenedor:
sudo /usr/local/bin/n8n-backup
cd /opt/n8n
sudo docker compose pull
sudo docker compose up -d
Las migraciones de la base de datos se ejecutan al arrancar. Revisa las notas de versión antes de saltar a una versión mayor, porque pueden incluir cambios incompatibles en nodos o variables de entorno. Si prefieres controlar cuándo cambias de versión, sustituye latest por una versión concreta en compose.yaml.
Solución de problemas
- 502 Bad Gateway: n8n no escucha en
127.0.0.1:5678. Compruebasudo docker compose psycurl http://127.0.0.1:5678/healthz. - El editor muestra "Connection lost": el WebSocket no pasa por Nginx. Revisa las cabeceras
UpgradeyConnectiondel bloque de servidor. Mismatching encryption keysen los registros: la clave de.envno coincide con la que se usó al crear los datos. Restaura el.envoriginal de la copia de seguridad.- Los nodos programados se ejecutan a la hora equivocada: revisa
TIMEZONEen.envy la zona horaria del propio flujo en sus ajustes.
Conclusión
Ya tienes n8n funcionando con PostgreSQL, detrás de Nginx con HTTPS, con un flujo de webhook probado y copias de seguridad diarias. Como siguientes pasos, conecta tus primeras credenciales para automatizar tareas reales, invita a tu equipo desde Settings y, si el volumen de ejecuciones crece, configura la poda del historial con las variables EXECUTIONS_DATA_PRUNE y EXECUTIONS_DATA_MAX_AGE.
