
Autohospedar n8n en producción: Docker Compose, PostgreSQL, HTTPS y queue mode
npx n8n o un solo contenedor sirve para probar, pero producción exige Docker Compose con PostgreSQL (SQLite se bloquea y no soporta queue mode), un N8N_ENCRYPTION_KEY persistente y respaldado (cifra cada credencial) y un reverse proxy con HTTPS y WEBHOOK_URL apuntando a tu dominio real. Agrega queue mode con Redis cuando una instancia deje de dar abasto.
Los defaults de n8n sirven para probar, no para producción
npx n8n o un solo contenedor te dejan encender n8n en un minuto para jugar. Pero producción es otra cosa. Necesitas Docker Compose con PostgreSQL (el SQLite por defecto se traba y no soporta queue mode), un N8N_ENCRYPTION_KEY persistente y respaldado —cifra cada credencial que guardas— y un reverse proxy con HTTPS con WEBHOOK_URL apuntando a tu dominio real. Cuando una sola instancia deja de dar abasto, agregas queue mode con Redis. Ese es todo el mapa; el resto del post es cómo cablearlo, con el docker-compose.yml y las variables de entorno exactas que corro en mi propio servidor.
Antes de tocar YAML, hay que entender por qué los defaults te fallan, porque cada default malo corresponde a una variable que vas a fijar a mano.
Por qué los defaults de n8n se rompen en producción (SQLite, sin TLS, llave efímera)
Tres defaults de n8n están pensados para arrancar rápido, no para aguantar carga real:
- SQLite como base de datos. Es un archivo local. Con más de un puñado de ejecuciones concurrentes ves lentitud en el editor y errores de database lock. Y lo más importante: SQLite no soporta queue mode, así que te cierra el único camino de escalado horizontal que tiene n8n. En producción usas PostgreSQL.
- Sin TLS. El editor levanta en HTTP plano en el puerto 5678. Exponer eso crudo a internet manda tus credenciales y tu sesión en texto claro. La terminación TLS va en un reverse proxy al frente.
- Llave de cifrado efímera. Si no fijas
N8N_ENCRYPTION_KEY, n8n genera una la primera vez y la guarda en el volumen.n8n. Si ese volumen se pierde o recreas el contenedor sin persistirlo, la llave cambia y todas tus credenciales guardadas quedan ilegibles. Hay que fijarla tú y respaldarla.
Defaults (para probar)
- SQLite: lento, database locks, sin queue mode
- HTTP plano en el puerto 5678
- N8N_ENCRYPTION_KEY autogenerada en el volumen
- Un solo proceso, sin escalado horizontal
- WEBHOOK_URL apunta a localhost
Producción
- PostgreSQL: rápido, sin locks, habilita queue mode
- HTTPS terminado en un reverse proxy
- N8N_ENCRYPTION_KEY fijada y respaldada
- EXECUTIONS_MODE=queue + Redis + workers cuando haga falta
- WEBHOOK_URL en tu dominio público real
Un docker-compose.yml con n8n + PostgreSQL
Docker Compose es el método recomendado para autohospedar: aísla n8n y su base de datos en contenedores y convierte las actualizaciones en un solo comando. Este es el esqueleto: n8n más PostgreSQL, con la base persistida en su propio volumen.
services:
postgres:
image: postgres:16
restart: unless-stopped
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
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
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.tudominio.com
- N8N_PROTOCOL=https
- N8N_PORT=5678
- WEBHOOK_URL=https://n8n.tudominio.com/
- N8N_EDITOR_BASE_URL=https://n8n.tudominio.com/
- GENERIC_TIMEZONE=America/Mexico_City
- TZ=America/Mexico_City
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:
Fíjate en un detalle de seguridad ya metido aquí: el mapeo de puerto es 127.0.0.1:5678:5678, no 5678:5678. Con eso n8n solo escucha en loopback y el reverse proxy es la única puerta desde afuera. Los secretos (POSTGRES_PASSWORD, N8N_ENCRYPTION_KEY) no van escritos en el YAML: van en un archivo .env junto al compose.
# .env — junto al docker-compose.yml, fuera de git
POSTGRES_PASSWORD=una-password-larga-y-aleatoria
N8N_ENCRYPTION_KEY=pega-aqui-la-que-generes-en-el-siguiente-paso
N8N_ENCRYPTION_KEY: genérala, fíjala y respáldala
Este es el valor más crítico de todo tu despliegue. n8n usa N8N_ENCRYPTION_KEY para cifrar todas tus credenciales —API keys, tokens de OAuth, passwords— antes de guardarlas en la base. Si la pierdes, pierdes acceso a cada credencial que tengas almacenada, aunque la base de datos esté intacta.
Genérala una vez con openssl y guárdala:
openssl rand -hex 32
Copia esa cadena a N8N_ENCRYPTION_KEY en tu .env y respáldala en tu gestor de secretos, no solo en el servidor. La regla mental: la base de datos y la llave son dos mitades. La base sin la llave es ruido cifrado; la llave sin la base no reconstruye nada. Necesitas ambas, guardadas por separado.
No dejes que n8n autogenere una llave que después no puedas reproducir. Fíjala explícitamente desde el día uno.
Apunta n8n a PostgreSQL: DB_TYPE y las variables DB_POSTGRESDB_*
Por defecto n8n usa SQLite. Para cambiar a PostgreSQL fijas DB_TYPE=postgresdb y el bloque de conexión completo:
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
El DB_POSTGRESDB_HOST es postgres —el nombre del servicio en el compose—, no localhost: dentro de la red de Docker, los contenedores se resuelven por nombre de servicio. El puerto 5432 es el estándar de PostgreSQL. Si te conectas a una base gestionada externa (RDS, Cloud SQL), cambias el host por el endpoint real y verificas que exija SSL.
Con PostgreSQL en su lugar quedan atrás los database locks de SQLite y, sobre todo, se destraba el queue mode más adelante. Este cambio no es opcional en producción. Si vas a construir workflows con webhooks reales encima de esto —por ejemplo, un agente que responde a eventos— tengo el paso a paso en automatiza con n8n y Claude.
Host, protocolo y WEBHOOK_URL: las variables que rompen los webhooks si fallan
n8n necesita saber su propia URL pública. Si no se la dices, asume localhost, y entonces todo webhook que genere —el que le pasas a Stripe, a WhatsApp, a un servicio externo— apunta a una dirección que nadie fuera del servidor puede alcanzar. El síntoma clásico: el workflow funciona al probarlo desde el editor pero el evento externo nunca llega.
Estas son las variables que lo arreglan:
N8N_HOST=n8n.tudominio.com
N8N_PROTOCOL=https
N8N_PORT=5678
WEBHOOK_URL=https://n8n.tudominio.com/
N8N_EDITOR_BASE_URL=https://n8n.tudominio.com/
N8N_HOSTes tu dominio.N8N_PROTOCOL=httpsle dice a n8n que las URLs que construye son HTTPS (el TLS real lo pone el proxy).WEBHOOK_URLes la que más gente equivoca: tiene que ser tu dominio público HTTPS, no localhost. De aquí sale la URL que copias en los servicios externos.N8N_EDITOR_BASE_URLfija la URL del editor.
Fija también la zona horaria con GENERIC_TIMEZONE y TZ, o tus nodos de Schedule dispararán en UTC y vas a perder una hora depurando por qué el cron corrió “tarde”.
Pon un reverse proxy con HTTPS al frente (nunca expongas el 5678 crudo)
El editor de n8n habla HTTP plano. La terminación TLS va en un reverse proxy —Caddy, Traefik o Nginx— que se sienta frente a n8n, atiende el 443 con tu certificado y reenvía a n8n en loopback. Nunca expongas el 5678 crudo a internet.
Caddy es el más corto porque gestiona el certificado de Let’s Encrypt solo. Un Caddyfile completo:
n8n.tudominio.com {
reverse_proxy 127.0.0.1:5678
}
Esas dos líneas te dan HTTPS automático con renovación. Con Nginx haces lo equivalente a mano —un server en el 443, tu ssl_certificate, y un proxy_pass http://127.0.0.1:5678; con los headers Upgrade/Connection para que el editor mantenga su WebSocket vivo. Con Traefik lo declaras por labels en el propio compose. Cualquiera de los tres sirve; el principio es el mismo: TLS afuera, n8n en loopback, el firewall cierra todo lo demás. Es la misma disciplina de cómo asegurar servidores MCP.
Escala con queue mode: EXECUTIONS_MODE=queue, Redis y procesos worker
Por defecto n8n corre en modo regular: el mismo proceso que sirve el editor también ejecuta cada workflow. Con carga sostenida eso se satura. Queue mode separa las dos cosas: el proceso principal encola los trabajos en Redis y procesos worker aparte los jalan y los ejecutan.
Se activa con:
EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis
Y levantas los workers con el comando n8n worker. En el compose agregas Redis y uno o más servicios worker que comparten la misma configuración de base y la misma llave de cifrado que el proceso principal:
redis:
image: redis:7
restart: unless-stopped
n8n-worker:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
command: worker
environment:
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
depends_on:
- postgres
- redis
Dos condiciones no negociables: queue mode requiere PostgreSQL y Redis —por eso SQLite te lo bloquea— y cada worker necesita el mismo N8N_ENCRYPTION_KEY, o no podrá descifrar las credenciales de los workflows que ejecuta. Escalas sumando réplicas de n8n-worker. Y un consejo de practicante: no llegues a queue mode por default. Es complejidad extra —un servicio más que respaldar y monitorear. Métela solo cuando una sola instancia de verdad no dé abasto, no “por si acaso”.
Si estás decidiendo si n8n es la herramienta correcta contra otras, lo comparo en agentes de IA vs herramientas de automatización.
Respalda la base de datos, la llave de cifrado y el volumen .n8n
Un respaldo de n8n tiene tres piezas, y las tres importan:
- La base de datos PostgreSQL —tus workflows, ejecuciones y credenciales cifradas.
- El
N8N_ENCRYPTION_KEY—sin esta llave, las credenciales de la base son ilegibles. - El volumen de datos
.n8nen/home/node/.n8n.
El punto que la gente aprende a la mala: la base sin la llave es inútil. Puedes restaurar un dump perfecto de PostgreSQL y aun así no poder usar una sola credencial si perdiste la llave. Respáldalas por separado —la base en tu rotación normal de backups, la llave en tu gestor de secretos.
Un dump de la base es un comando:
docker compose exec postgres pg_dump -U n8n n8n > n8n-backup-$(date +%F).sql
Prográmalo, mándalo fuera del servidor y —de vez en cuando— prueba una restauración de verdad. Un backup que nunca restauraste no es un backup, es una esperanza.
- PostgreSQL (pg_dump)Workflows, ejecuciones y credenciales cifradas — en tu rotación de backups
- N8N_ENCRYPTION_KEYEn tu gestor de secretos, aparte de la base — descifra las credenciales
- Volumen .n8n/home/node/.n8n — respáldalo con el resto del stack
Actualizar n8n: baja la imagen y recrea el contenedor
Con Compose, actualizar es de una línea conceptual: baja la imagen nueva y recrea los contenedores. Tus datos viven en volúmenes y en PostgreSQL, así que sobreviven el recreado.
docker compose pull
docker compose up -d
pull trae la última imagen de docker.n8n.io/n8nio/n8n; up -d recrea solo lo que cambió y deja PostgreSQL y tus volúmenes intactos. Aun así, respalda la base antes de subir de versión —sobre todo entre versiones mayores, donde puede haber migraciones de esquema— y lee el changelog. Si corres queue mode, recuerda actualizar la imagen de los workers junto con la del proceso principal para que no queden en versiones distintas.
Checklist de seguridad para producción
Antes de ponerlo de cara al público, revisa esto:
- No expongas el 5678 crudo. Termina TLS en el proxy y mapea el puerto solo a
127.0.0.1. - Firewall. Deja abiertos 80/443 hacia el proxy; cierra todo lo demás.
- Habilita owner/auth de n8n para que el editor no quede abierto.
N8N_ENCRYPTION_KEYfija y respaldada —generada conopenssl rand -hex 32, guardada en tu gestor de secretos.- Secretos en env o secret files, nunca en el workflow. No pegues API keys en un nodo; úsalas como credenciales.
- PostgreSQL en la red interna de Docker, nunca publicado a internet.
- HTTPS de punta a punta con
N8N_PROTOCOL=httpsyWEBHOOK_URLen tu dominio real. - Respaldos probados —base más llave— con una restauración de prueba ya hecha.
Con esto tienes n8n corriendo como un servicio de producción de verdad: PostgreSQL debajo, HTTPS al frente, credenciales cifradas con una llave que controlas y respaldas, y un camino claro a queue mode cuando el volumen lo pida. Si desde aquí quieres darle a tus workflows acceso a datos reales de forma segura, sigue con conectar un agente de IA a tus datos con MCP. Todo esto vive en el hub de agentes de IA.
Fuentes oficiales: Instalación con Docker (docs.n8n.io), Variables de entorno (docs.n8n.io) y Queue mode / scaling (docs.n8n.io).