Saltar al contenido

n8n en Docker: rápido de montar, serio de mantener. Despliega en tu empresa.

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 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.1 y 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 distingue stable (producción) y beta (más reciente, potencialmente inestable).

Paso 1: crea estructura de proyecto

  1. Crea una carpeta, por ejemplo /opt/n8n.
  2. Dentro, crea docker-compose.yml y .env.
sudo mkdir -p /opt/n8n/{secrets,local-files}
sudo chown -R $USER:$USER /opt/n8n
cd /opt/n8n
touch compose.yaml .env

Paso 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 _FILE en variables de DB (útil con secrets).
  • Postgres oficial soporta POSTGRES_PASSWORD_FILE cargando 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: bridge

Paso 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_URL y N8N_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

  1. docker compose up -d
  2. docker compose ps
  3. 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 a n8n_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 ~/.n8n en 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 cualquier local-files bind mount si lo usas para leer/escribir ficheros desde workflows.
    • El .env no 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

  1. No uses :latest. Pinnea una versión concreta en el compose.
  2. 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

  1. Edita el compose y sube la versión (ej. 1.80.01.81.0).
  2. docker compose pull
  3. docker compose up -d
  4. 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.yml usa volumen persistente para /home/node/.n8n
  • El .env existe, no está en Git, y tiene N8N_ENCRYPTION_KEY definida
  • 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.


Juan Ibero

Juan Ibero

Inmerso en la Evolución Tecnológica. Ingeniero Informático especializado en la gestión segura de entornos TI e industriales, con un profundo énfasis en seguridad, arquitectura y programación. Siempre aprendiendo, siempre explorando.