18 Sep 2026 · 11 min lectura · Automatizacion
n8n con Docker Compose: instalar n8n en tu propio servidor
Por Nico Manzaneque
Toda la parte determinista de mi negocio corre en un n8n que vive en un servidor mío. No en n8n Cloud. Lo monté con Docker Compose y llevo años actualizándolo, rompiéndolo y arreglándolo. Esta guía es lo que me hubiera gustado leer la primera vez: un docker-compose.yml que funciona, HTTPS sin sufrir, backups que se restauran y la lista de errores que te vas a comer sí o sí.

Si todavía estás decidiendo entre n8n, Make o Zapier, primero lee la comparativa. Aquí doy por hecho que ya has elegido n8n y quieres tenerlo en tu servidor.
Por qué Docker Compose y no npm o Docker a pelo
n8n se puede instalar de tres formas. Las tres funcionan. Solo una se mantiene bien.
| Forma | Qué implica | Cuándo tiene sentido |
|---|---|---|
| npm | Node instalado en el servidor, dependes de su versión, actualizas a mano | Probar en tu portátil diez minutos |
Docker (docker run) | Un comando de doce líneas que nadie recuerda al mes | Laboratorio rápido |
| Docker Compose | Un archivo de texto que describe todo el sistema | Producción, aunque sea una pyme de tres personas |
Con Compose, el archivo es la documentación. Si mañana el servidor muere, copias el docker-compose.yml, el .env y el backup del volumen a otra máquina y levantas exactamente lo mismo.
Una aclaración sobre "n8n github": el código está en el repositorio n8n-io/n8n y ahí se publican las releases. No necesitas clonarlo ni compilar nada. La imagen oficial n8nio/n8n está en Docker Hub y es la que vamos a usar. El repositorio sirve para leer las notas de cada versión antes de actualizar.
Lo que necesitas antes de empezar
Cuatro cosas. Si te falta una, no sigas.
- Un servidor Linux con Docker Engine y el plugin de Compose. Un VPS de 2 GB de RAM va sobrado para una pyme.
- Un dominio o subdominio apuntando a la IP del servidor con un registro A. Sin dominio no hay HTTPS, y sin HTTPS los webhooks de Telegram, Meta o Stripe no funcionan.
- Puertos 80 y 443 abiertos en el firewall del servidor y del proveedor.
- Una clave de cifrado que generas tú y guardas en dos sitios. Es la pieza más importante de todo el sistema y luego verás por qué.
¿Y n8n en Windows? Docker Desktop con WSL2 ejecuta el mismo docker-compose.yml sin cambiar una línea. Vale para aprender y probar flujos en local. Para producción no: un portátil que se apaga por la noche no es un servidor. Si necesitas recibir webhooks mientras desarrollas en Windows, un túnel tipo Cloudflare Tunnel o ngrok te da una URL pública temporal.
El docker-compose.yml completo
Tres servicios: n8n, Postgres y Caddy como reverse proxy con HTTPS automático. Crea una carpeta, por ejemplo /opt/n8n, y dentro tres archivos: docker-compose.yml, .env y Caddyfile.
services:
n8n:
image: n8nio/n8n:${N8N_VERSION}
container_name: n8n
restart: unless-stopped
environment:
- N8N_HOST=${N8N_HOST}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_EDITOR_BASE_URL=https://${N8N_HOST}/
- N8N_WEBHOOK_URL=https://${N8N_HOST}/
- N8N_PROXY_HOPS=1
- GENERIC_TIMEZONE=Europe/Madrid
- TZ=Europe/Madrid
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_DIAGNOSTICS_ENABLED=false
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16
container_name: n8n_postgres
restart: unless-stopped
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
caddy:
image: caddy:2
container_name: n8n_caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
volumes:
n8n_data:
name: n8n_data
postgres_data:
name: n8n_postgres_data
caddy_data:
caddy_config:
Fíjate en que n8n no publica ningún puerto al exterior. Solo Caddy abre el 80 y el 443. n8n y Postgres hablan por la red interna de Compose. Menos superficie expuesta, menos problemas.
El .env que va al lado:
N8N_VERSION=2.39.7
N8N_HOST=n8n.tudominio.com
N8N_ENCRYPTION_KEY=pon-aqui-la-clave-que-generes
POSTGRES_DB=n8n
POSTGRES_USER=n8n
POSTGRES_PASSWORD=una-contrasena-larga-y-aleatoria
La clave de cifrado se genera antes de arrancar nada:
openssl rand -hex 32
Y el Caddyfile, que sorprende por lo corto:
n8n.tudominio.com {
reverse_proxy n8n:5678
}
Eso es todo el reverse proxy. Caddy pide el certificado a Let's Encrypt, lo renueva solo, redirige HTTP a HTTPS y pasa los websockets del editor sin configurar nada más. He usado Traefik y Nginx para esto. Para una instancia de n8n, Caddy gana en líneas de configuración que puedes romper.
Qué hace cada variable
| Variable | Para qué sirve |
|---|---|
N8N_HOST, N8N_PROTOCOL, N8N_EDITOR_BASE_URL | Con qué dominio y protocolo se llega a n8n desde fuera. Sin ellas, el editor genera enlaces a localhost |
N8N_WEBHOOK_URL | URL base de los webhooks de test y producción detrás del proxy. Sin ella, webhooks con http://localhost:5678 que nadie externo puede llamar |
N8N_PROXY_HOPS | Cuántos proxies hay delante para que n8n confíe en las cabeceras X-Forwarded y vea la IP real |
GENERIC_TIMEZONE y TZ | Zona horaria de los Schedule Trigger. Sin ellas, tus crons de las 8:00 se disparan a otra hora |
N8N_ENCRYPTION_KEY | Clave con la que se cifran todas las credenciales en la base de datos. Si no la defines, n8n genera una y la guarda en el volumen |
N8N_DIAGNOSTICS_ENABLED | Telemetría anónima hacia n8n. Yo la apago |
Un aviso sobre nombres. Hasta hace poco la variable de webhooks se llamaba WEBHOOK_URL. Desde n8n 2.35 el nombre oficial es N8N_WEBHOOK_URL; el antiguo sigue funcionando pero saca un aviso de deprecación al arrancar. Si copias un compose de un tutorial de 2024 verás WEBHOOK_URL. No está mal, pero ya sabes por qué avisa el log.
Sin Postgres
n8n usa SQLite por defecto y para una instancia pequeña funciona perfectamente. Para la versión mínima, borra el servicio postgres, su volumen, el bloque depends_on de n8n y las seis variables que empiezan por DB_. La base SQLite vive dentro de /home/node/.n8n, así que el backup del volumen la incluye. Pasa a Postgres cuando tengas muchas ejecuciones por hora o quieras escalar con workers en modo cola. Si sabes que la instancia va a crecer, empieza ya con Postgres: migrar después es posible pero es un trabajo extra.
Arrancar y entrar por primera vez
cd /opt/n8n
docker compose up -d
docker compose logs -f n8n
En los logs verás las migraciones de base de datos y al final la URL del editor. Abre https://n8n.tudominio.com, crea la cuenta de propietario y ya estás dentro.
Comprueba dos cosas antes de construir nada. Crea un workflow con un nodo Webhook y mira la URL de producción: tiene que empezar por https://n8n.tudominio.com/webhook/. Y crea un Schedule Trigger a una hora concreta: tiene que aparecer en hora de Madrid. Si algo falla, vuelve a la tabla de variables.
Backups: la clave, el volumen y la base de datos
Un servidor sin backup no es un servidor, es una apuesta. En n8n hay tres cosas que respaldar.
La clave de cifrado. Si la pusiste en el .env, ya la tienes. Guarda además una copia fuera del servidor, en el gestor de contraseñas del equipo. Sin esa clave, un backup de la base de datos es un archivo con credenciales cifradas que nadie puede leer.
El volumen n8n_data. Contiene el archivo config con la clave (si dejaste que n8n la generase), la base SQLite si no usas Postgres y los binarios de ejecuciones.
mkdir -p /opt/n8n/backups
docker run --rm \
-v n8n_data:/data:ro \
-v /opt/n8n/backups:/backup \
alpine tar czf /backup/n8n_data_$(date +%F).tar.gz -C /data .
Por eso puse name: n8n_data en los volúmenes del compose. Sin ese name, Compose antepone el nombre de la carpeta y acabas con n8n_n8n_data, que despista al hacer backups a mano.
La base de datos Postgres, si la usas:
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > /opt/n8n/backups/n8n_db_$(date +%F).sql.gz
Y un extra que me ha salvado más de una vez: exportar los workflows en JSON con la CLI de n8n, para poder importarlos en otra instancia aunque la base esté corrupta.
docker compose exec n8n n8n export:workflow --all --output=/home/node/.n8n/workflows-backup.json
Mete los tres comandos en un backup.sh, prográmalo en el crontab cada noche y copia la carpeta backups fuera de la máquina con rsync o rclone. Un backup que vive en el mismo disco que el original solo te protege de ti mismo, no del proveedor. Y prueba la restauración una vez en otra máquina. Si nunca lo has hecho, no tienes backup. Tienes un archivo.
Actualizar n8n sin romper nada
n8n publica versiones muy a menudo. A fecha de hoy la rama estable está en la 2.39 y hay una 2.40 en beta. Por eso fijo la versión en el .env y no uso la etiqueta latest: un docker compose pull inocente un viernes por la tarde no debería cambiar la versión mayor de tu motor de automatización.
El procedimiento, siempre en este orden:
- Backup completo con el script de arriba.
- Leer las notas de la versión nueva en las releases de GitHub. Buscar la palabra "breaking".
- Cambiar
N8N_VERSIONen el.env. - Actualizar:
docker compose pull
docker compose up -d
docker compose logs -f n8n
En el log verás las migraciones. Si algo falla, no bajes de versión sin más: las migraciones de base de datos no se deshacen solas. Restaura el backup previo y vuelve a la versión anterior en el .env. La etiqueta next es beta y no es para producción. Cada pocas actualizaciones, docker image prune para liberar disco.
Errores típicos y cómo se arreglan
Esta tabla es la mayoría de los mensajes que recibo cuando alguien monta n8n por su cuenta.
| Síntoma | Causa | Arreglo |
|---|---|---|
Los webhooks muestran http://localhost:5678/webhook/... | Falta N8N_WEBHOOK_URL (o WEBHOOK_URL) | Añádela con la URL pública y reinicia con docker compose up -d |
| El Telegram Trigger falla al registrar el webhook | La URL pública no es HTTPS o no llega al servidor | Revisa DNS, puertos 80/443 y el certificado en docker compose logs caddy |
| "Credentials could not be decrypted" o clave no coincide | El contenedor arranca con una N8N_ENCRYPTION_KEY distinta de la que cifró las credenciales, o el volumen se recreó | Recupera la clave original del .env o del archivo config del backup; si se perdió, hay que volver a introducir todas las credenciales |
EACCES: permission denied sobre /home/node/.n8n | Bind mount a una carpeta del host que pertenece a root; el contenedor corre como usuario node (uid 1000) | sudo chown -R 1000:1000 ./carpeta o usa un volumen con nombre, como en este compose |
| Aviso sobre permisos del archivo de configuración | El archivo config del volumen es legible por otros usuarios | N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true para que n8n lo deje en 0600 |
| Página de error sobre "secure cookie" al entrar por IP | Entras por http://IP:5678; n8n exige HTTPS para la cookie de sesión | Entra por el dominio con HTTPS. Solo en laboratorio local, N8N_SECURE_COOKIE=false |
El de la clave de cifrado merece una frase más. Es el único error de la lista sin arreglo si no hiciste los deberes. Por eso la clave va en el .env desde el primer día y con copia fuera del servidor. Cero excepciones.
Self-host o n8n Cloud: mi criterio
n8n Cloud es un producto serio. A 18 de septiembre de 2026, en su web el plan Starter cuesta 20 euros al mes con 2.500 ejecuciones y el Pro 50 euros al mes con 10.000, ambos con facturación anual. Un VPS para self-host cuesta menos al mes. Pero el precio no es el criterio.
| Elige n8n Cloud si... | Elige self-host si... |
|---|---|
| Nadie en tu equipo quiere tocar un servidor | Alguien puede dedicar una hora al mes a mantenimiento |
| Tus flujos no manejan datos sensibles | Trabajas con datos de clientes, salud, finanzas o legal |
| Ejecutas unos miles de veces al mes como mucho | Tienes flujos que corren cada minuto |
| Prefieres pagar por no pensar en backups | Quieres control total y coste fijo |
Yo estoy en self-host por dos motivos. Los datos de mis clientes no salen de una máquina que controlo. Y ejecuto muchas cosas pequeñas muy a menudo, así que el pago por ejecución no me encaja. Si empezara sin nadie técnico cerca, empezaría en Cloud y migraría después: el export de workflows en JSON convierte la migración en un rato, no en un proyecto.
Lo que n8n hace en NMC y lo que no
Aquí viene lo que casi ningún tutorial cuenta. En NMC tenemos una regla de la casa: n8n solo para lo determinista.
Determinista quiere decir que el flujo es fijo. Entra un webhook, se transforman unos campos, se escriben en Notion, se avisa por chat. Los mismos pasos cada vez, con datos distintos. Eso es n8n y lo hace de maravilla: barato, visible, fácil de mantener, con sticky notes en el canvas para que el equipo entienda cada rama.
Lo que no va en n8n es lo agéntico: cuando el siguiente paso depende de lo que se encuentra en el anterior. Un ejemplo real: leer un hilo de correo, decidir si es una factura, consultar el banco para ver si el cobro entró y, según eso, crear un registro o avisar a la gestora. Ese flujo no es fijo. Cada correo pide consultas distintas. Modelarlo en n8n te obliga a dibujar cada rama posible y acabas con el monstruo de 300 nodos que nadie se atreve a tocar.
Para eso usamos un agente: Claude Code en el propio servidor con acceso a las herramientas, o el Agent SDK cuando hace falta algo más robusto. El agente decide. Y cuando la decisión termina en una acción determinista, dispara un workflow de n8n por webhook. n8n es el brazo, no el cerebro. Nunca al revés.
Primero el proceso. Después el agente. Y n8n en el sitio que le corresponde. Cómo se decide qué parte de un proceso va a cada capa lo explico en agentes de IA para empresas. Si prefieres que lo montemos juntos, mira cómo trabajo con empresas.
Preguntas frecuentes
¿n8n self-hosted es gratis de verdad?
Sí para uso interno. La licencia Sustainable Use permite ejecutarlo en tu servidor sin pagar licencia, con workflows y usuarios ilimitados. Lo que no permite es revenderlo como servicio a terceros. Pagas el servidor y tu tiempo de mantenimiento.
¿Puedo usar Traefik o Nginx en lugar de Caddy?
Sí. n8n funciona detrás de cualquier reverse proxy que soporte websockets. Caddy es mi elección porque el certificado y la renovación vienen incluidos con dos líneas. Con Nginx necesitas certbot aparte y con Traefik un puñado de labels.
¿Qué pasa si pierdo la clave de cifrado?
Los workflows siguen ahí, pero todas las credenciales quedan ilegibles y hay que volver a introducirlas a mano. Es recuperable, pero es un día perdido. Guarda la clave en el .env y en el gestor de contraseñas desde el primer minuto.