Todos dicen que es fácil. Mienten (un poco). Montar n8n con Docker es sencillo; lo difícil es no convertirlo en una bomba cuando empieces a depender de tus flujos para cosas serias: facturas, tickets, alertas, integraciones… lo típico que no quieres perder por un docker compose down mal tirado.
Voy al grano: aquí tienes una guía práctica para desplegar n8n, definir una estrategia de backups que sirva para recuperar, y hacer upgrades seguros.
Por qué autoalojar n8n y por qué el día 2 te va a pedir factura
Autoalojar n8n mola por control: datos en tu infraestructura, más libertad para integrar, y coste predecible. El lado B es que tú te comes (lo tienes explicado en n8n sin humo):
- Persistencia de datos (workflows, credenciales, ejecuciones).
- Backups y restauraciones (restaurar, no “tener un zip”).
- Upgrades que cambian cosas (a veces sutiles, a veces no).
El rollo es que n8n suele empezar como “una automatización para probar” y acaba siendo infraestructura. Y la infraestructura sin backups ni upgrades controlados… bueno, no exactamente infraestructura: más bien una apuesta.
1) Despliegue inicial con Docker Compose MVP
Asumo que ya tienes Docker y Docker Compose funcionando en el host. Si no, primero arregla eso (y si estás en Debian, te dejo una lectura que suele venir bien cuando toca mantener sistemas vivos: Actualizar de Debian 9 Stretch a Debian 10 Buster. Mantén tus sistemas seguros.).
Paso 0: supuestos mínimos de “producción”
- No expongas el puerto 5678 a Internet: publícalo solo en
127.0.0.1y entra por un reverse proxy (Nginx/Traefik) con TLS. - Usa PostgreSQL (SQLite es el default, pero para producción es preferible Postgres).
- Fija (pin) versión de n8n; no uses
:latest. n8n distinguestable(producción) ybeta(más reciente, potencialmente inestable).
Paso 1: crea estructura de proyecto
- Crea una carpeta, por ejemplo
/opt/n8n. - Dentro, crea
docker-compose.ymly.env.
sudo mkdir -p /opt/n8n/{secrets,local-files}
sudo chown -R $USER:$USER /opt/n8n
cd /opt/n8n
touch compose.yaml .envPaso 2: secretos (fuera de Git)
Crea dos ficheros:
- Clave de cifrado de credenciales (persistente). Por defecto n8n genera una aleatoria en el primer arranque; en producción conviene fijarla.
- Password de Postgres vía
_FILE(compatible con Docker secrets). docs.n8n.io+1
# Genera clave larga (ejemplo) openssl rand -hex 32 | tee /opt/n8n/secrets/n8n_encryption_key.txt >/dev/null openssl rand -base64 32 | tee /opt/n8n/secrets/postgres_password.txt >/dev/null chmod 600 /opt/n8n/secrets/*.txt
Paso 2: docker-compose mínimo con volúmenes (n8n + PostgreSQL, sin exponer 5678 a Internet)
Este compose es pequeño: un contenedor, un volumen persistente, y configuración por variables. Lo puedes poner detrás de un reverse proxy (Traefik/Nginx) más adelante. A mi personalmente me gusta más Nginx.
Ten en cuenta también:
- n8n soporta Postgres y permite
_FILEen variables de DB (útil con secrets). - Postgres oficial soporta
POSTGRES_PASSWORD_FILEcargando desde/run/secrets/.... - Reducir lo que se guarda de ejecuciones + pruning evita que la base crezca sin control.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_DB: n8n
POSTGRES_PASSWORD_FILE: /run/secrets/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
secrets:
- postgres_password
networks:
- internal
n8n:
image: docker.n8n.io/n8nio/n8n:2.0.3 # ejemplo: fija versión (stable según docs)
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
# Solo local (reverse proxy delante)
ports:
- "127.0.0.1:5678:5678"
env_file:
- .env
environment:
# DB (PostgreSQL)
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD_FILE: /run/secrets/postgres_password
# Cifrado credenciales (persistente)
N8N_ENCRYPTION_KEY_FILE: /run/secrets/n8n_encryption_key
# Reverse proxy / URLs
# n8n construye webhooks con N8N_PROTOCOL/N8N_HOST/N8N_PORT, pero detrás de proxy debes fijar WEBHOOK_URL y N8N_PROXY_HOPS=1
N8N_PROXY_HOPS: 1
# Timezone
GENERIC_TIMEZONE: Europe/Madrid
TZ: Europe/Madrid
# Hardening básico
N8N_BLOCK_ENV_ACCESS_IN_NODE: "true"
N8N_RESTRICT_FILE_ACCESS_TO: "/files"
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: "true"
N8N_SECURE_COOKIE: "true"
N8N_SAMESITE_COOKIE: "lax"
# Control crecimiento de BD (recomendado)
EXECUTIONS_DATA_SAVE_ON_SUCCESS: "none"
EXECUTIONS_DATA_SAVE_ON_ERROR: "all"
EXECUTIONS_DATA_SAVE_ON_PROGRESS: "false"
EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS: "false"
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "336" # 14 días (default)
EXECUTIONS_DATA_PRUNE_MAX_COUNT: "10000" # default
volumes:
- n8n_data:/home/node/.n8n
- ./local-files:/files
secrets:
- postgres_password
- n8n_encryption_key
networks:
- internal
secrets:
postgres_password:
file: ./secrets/postgres_password.txt
n8n_encryption_key:
file: ./secrets/n8n_encryption_key.txt
volumes:
n8n_data:
postgres_data:
networks:
internal:
driver: bridgePaso 4: .env de ejemplo (sin secretos reales)
Ojo: esto es un ejemplo. No metas secretos en Git. Y si estás en equipo, tratad el .env como material sensible.
# URL pública del editor (emails / redirects SAML, etc.) N8N_EDITOR_BASE_URL=https://n8n.tu-dominio.example N8N_HOST=n8n.tu-dominio.example N8N_PORT=5678 N8N_PROTOCOL=https # Webhooks (imprescindible con reverse proxy) WEBHOOK_URL=https://n8n.tu-dominio.example/
N8N_EDITOR_BASE_URL y N8N_ENCRYPTION_KEY están documentadas como variables de despliegue.
Paso 5: reverse proxy (Nginx) — lo mínimo correcto
Puntos obligatorios:
WEBHOOK_URLyN8N_PROXY_HOPS=1.- En el último proxy, pasa
X-Forwarded-For/Host/Proto.
Ejemplo de server (resumen):
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Paso 6: arranca y valida
- docker compose up -d
- docker compose ps
- Entra por
https://n8n.tu-dominio.example, crea el usuario inicial y prueba un workflow simple (Webhook → Respond).
Si vas a exponerlo a Internet, no lo hagas “a pelo” con el puerto 5678 abierto y ya. Pon reverse proxy con TLS, y si puedes, alguna capa extra (IP allowlist, VPN, o al menos autenticación decente). Si te interesa el enfoque más de programa y control, te dejo otra: El Plan Director de Seguridad. No es n8n, pero la mentalidad aplica.

2) Backups: qué guardar, cómo y cada cuánto
La pregunta no es “¿tengo backup?”. La pregunta es: “¿puedo restaurar en <X horas, con un runbook, y sin improvisar?”.
Además: haz backup “antes de tocar nada” (updates, cambios de proxy, cambios de DB). n8n publica versiones con frecuencia y conviene tratar cada cambio como potencialmente disruptivo.
¿Qué debes respaldar?
- Volumen de datos: en este caso,
/home/node/.n8n(mapeado an8n_data).- Si usas SQLite, aquí va todo: DB + clave de cifrado.
- Aunque migres a Postgres, aquí sigue habiendo artefactos importantes (y, si no fijas clave, aquí acaba guardada). n8n crea una clave de cifrado y la guarda en
~/.n8nen el primer arranque.
- La clave de cifrado (N8N_ENCRYPTION_KEY)
- Debe ser estable entre reinstalaciones/restores. Si cambias la clave, las credenciales guardadas dejan de ser desencriptables (operativamente es un “data loss” aunque tengas la DB). n8n permite fijarla por variable de entorno.
- Tu “infra como código”
compose.yaml/docker-compose.yml, configuraciones del reverse proxy, y cualquierlocal-filesbind mount si lo usas para leer/escribir ficheros desde workflows.- El
.envno debería vivir “en claro” en Git: cifrado (SOPS/age) o en un vault/secret manager.
- Base de datos (si usas Postgres)
- n8n soporta SQLite por defecto y Postgres como opción recomendada cuando creces (mejor tooling de backup/restore y operación).
- Aquí el backup “bueno” es pg_dump (lógico) o base backup (físico), según tus requisitos.
- Backup lógico adicional (opcional pero muy útil): export de workflows
- n8n permite exportar/importar workflows (UI/CLI) en JSON. Útil para migraciones, DR rápido y “última línea de defensa”.
Yo personalmente empiezo con el volumen (porque es lo que has montado) y, cuando el uso se vuelve serio, paso a Postgres. No porque Docker volume sea “malo”, sino porque quieres tooling de backup/restore más estándar, y menos magia.
También puedes matar moscas a cañonazos, y copiar las máquinas enteras con reglas 321.
Estrategia simple (que funciona)
Define dos números:
- RPO (cuánto dato puedes perder): p. ej., 24h.
- RTO (en cuánto debes volver): p. ej., 2h.
Recomendación típica:
- Frecuencia: diario (producción) / semanal (lab).
- Retención (ejemplo): 14 diarios + 8 semanales + 12 mensuales.
- Offsite: siempre (S3/NAS remoto/otro CPD).
- Inmutabilidad: si puedes (Object Lock / snapshots inmutables).
- Verificación: 1 restore de prueba/mes (automatizado si es posible).
Comando/script de backup
A) Si estás en SQLite (solo volumen)
Idea: parar n8n, hacer tar del volumen, arrancar.
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="/opt/n8n"
BACKUP_DIR="/opt/backups/n8n"
TS="$(date -u +%F_%H%M%S)"
cd "$PROJECT_DIR"
mkdir -p "$BACKUP_DIR"
# 1) Para n8n para evitar backups inconsistentes
docker compose stop n8n
# 2) Localiza el volumen real (puede llamarse <proyecto>_n8n_data si no fijaste "name:")
VOL="$(docker volume ls -q | grep -E '(^|_)n8n_data$' | head -n1)"
# 3) Backup
docker run --rm \
-v "${VOL}:/data:ro" \
-v "${BACKUP_DIR}:/backup" \
alpine:3.20 \
sh -c "tar -czf /backup/n8n_data_${TS}.tgz -C /data ."
# 4) Arranca de nuevo
docker compose start n8n
Esto cubre ~/.n8n (incluye, en setup típico, la DB SQLite y la clave).
B) Si usas Postgres (recomendado cuando hay negocio)
Idea: backup de DB + backup de /home/node/.n8n (y local-files si aplica).
1) Dump de Postgres (pg_dump en formato custom):
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="/opt/n8n"
BACKUP_DIR="/opt/backups/n8n"
TS="$(date -u +%F_%H%M%S)"
cd "$PROJECT_DIR"
mkdir -p "$BACKUP_DIR"
# Dump lógico (no requiere parar n8n en la mayoría de casos)
docker compose exec -T postgres \
pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
--format=custom --no-owner --no-acl \
> "${BACKUP_DIR}/n8n_pg_${TS}.dump"
2) Backup del volumen /home/node/.n8n:
VOL="$(docker volume ls -q | grep -E '(^|_)n8n_data$' | head -n1)"
docker run --rm \
-v "${VOL}:/data:ro" \
-v "/opt/backups/n8n:/backup" \
alpine:3.20 \
sh -c "tar -czf /backup/n8n_data_${TS}.tgz -C /data ."
n8n documenta que en despliegues con Docker se suele montar n8n_data en /home/node/.n8n (donde vive SQLite y la clave de cifrado en setups típicos).
Y recuerda: Postgres se configura por variables DB_TYPE=postgresdb.
3) Upgrades seguros: versionado, pruebas y rollback rápido
Actualizar n8n no es “pull y a correr”. El patrón que me funciona:
Paso 1: pin de versión y salto controlado
- No uses
:latest. Pinnea una versión concreta en el compose. - Si llevas meses sin tocarlo, no saltes 20 versiones de golpe. Ve por tramos (menos épico, más estable).
Paso 2: backup antes del upgrade
Antes de tocar nada: ejecuta el backup. Y si usas Postgres, dump también. Esto es el cinturón de seguridad.
Paso 3: upgrade con smoke test
- Edita el compose y sube la versión (ej.
1.80.0→1.81.0). docker compose pulldocker compose up -d- Smoke test (5 minutos):
- Login ok
- Un workflow manual (run once) ok
- Un webhook/trigger real ok
- Revisar logs:
docker logs --tail=200 -f
Paso 4: rollback por si algo huele a chamiusquina
Rollback = volver a la imagen anterior y levantar. Si hay migraciones de datos de por medio, aquí es donde empieza lo divertido… por eso pinneamos y hacemos upgrades escalonados. Si el upgrade toca esquema/credenciales, tu plan de rollback real es: restaurar backup.
Advertencias: 3 errores comunes
- Olvidar volúmenes. Si no persistes
/home/node/.n8n, el día que recrees el contenedor, adiós workflows y credenciales. Y sí, pasa. - Usar
:latest. Es la ruleta rusa de los upgrades. Un día te mete un cambio incompatible y te rompe automatizaciones en silencio (que es peor). - Backups en el mismo host. Si el disco muere, si te cifran la máquina, o si borras
/opt“sin querer”, te quedas igual de vacío.
Y una más, de propina: no probar restauración. .
Checklist final (6 puntos)
- El
docker-compose.ymlusa volumen persistente para/home/node/.n8n - El
.envexiste, no está en Git, y tieneN8N_ENCRYPTION_KEYdefinida - Backups automatizados con retención (y copia offsite)
- Restauración probada al menos una vez (ideal: mensual)
- Versiones pinneadas y procedimiento de upgrade con smoke test + rollback
- Monitorización mínima: espacio en disco, logs, y alertas si el contenedor reinicia.
Si haces esto, te garantizo que lo haces mejor que el 90% de los mortales (datos científicos sacados de mis cojones en bata). Esta es la base, el mínimo, lo indispensable para ser un buen profesional.
