n8n en producción: de demo a Kubernetes con carga real
Por qué n8n en producción es otra cosa
La primera vez que instalas n8n, todo funciona. Un docker run con la imagen oficial, SQLite como base, y en cinco minutos tienes un editor visual donde arrastras nodos para conectar APIs, webhooks y bases de datos. Es la mejor demo de un motor de workflows que existe.
El problema aparece cuando lo expones a tráfico real. La misma instancia que movió 50 ejecuciones al día empieza a quedarse sin memoria con 5,000. Los webhooks fallan porque el proceso principal está ocupado ejecutando un workflow largo. La base SQLite se corrompe porque dos réplicas intentan escribir al mismo archivo. Y terminas entendiendo por qué el equipo de Fairwinds publicó en el blog de CNCF lo que aprendieron corriendo n8n en producción sobre EKS durante más de un año.
Este post es la versión para sysadmins Linux que no corren en EKS. La mayoría de nosotros usamos k3s, microk8s, o un VPS con docker-compose. Pero los problemas son los mismos: workers, webhooks, PostgreSQL, Redis, persistencia, secretos, y un plan de upgrade que no rompa el historial de ejecuciones. Lo que voy a contarte aplica a cualquier orquestador, no solo a Kubernetes gestionado en AWS.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 4-6 horas si ya tienes k3s o k8s; 1-2 días si partes de cero con n8n demo.
- Requisitos: cluster Kubernetes (k3s sirve), PostgreSQL 13+, Redis 6+, object storage (S3, MinIO, o local), Helm 3.10+.
- Resultado: n8n con main + workers + webhooks separados, cola Redis, base PostgreSQL externa, secretos sincronizados desde un vault, y un upgrade path testeado.
- Troubleshooting: si los webhooks fallan, revisa el ingress; si los workflows se quedan colgados, mira la cola Redis; si la base se corrompe, restaura desde snapshot y no uses SQLite en producción.
El modo demo vs el modo producción
Por defecto, n8n corre en modo main único: un solo proceso que ejecuta triggers, webhooks y workflows. Esto es perfecto para aprender y para integraciones que corren unas pocas veces al día. No hay cola, no hay workers, no hay nada que coordinar.
Cuando entras a producción, ese modelo colapsa por tres razones:
- Webhooks y ejecuciones compiten por el mismo proceso. Un workflow que corre durante 30 segundos bloquea el trigger de otro webhook entrante. En el mejor caso se acumulan llamadas; en el peor, los webhooks devuelven timeout y el cliente externo marca el job como fallido.
- SQLite no escala horizontalmente. Dos réplicas de n8n con SQLite corrompen la base. La solución nativa es PostgreSQL, pero requiere migrar las tablas, ajustar permisos, y configurar backups consistentes.
- Los secretos viven en el filesystem del pod. Una variable de entorno
N8N_ENCRYPTION_KEYguardada como Kubernetes Secret está bien hasta que rotas el secreto y pierdes acceso a todas las credenciales encriptadas en workflows. La solución es un secret manager externo (AWS Secrets Manager, HashiCorp Vault, o Bitnami Sealed Secrets) sincronizado al cluster.
La transición de demo a producción es, en esencia, separar las responsabilidades del proceso único en tres: main (gestiona triggers y metadata), workers (ejecutan workflows desde la cola), y webhook processors (reciben HTTP entrante y lo enqueuean).
Arquitectura: main, workers, webhooks separados
El patrón estándar de n8n en producción es el modo queue mode. Lo activas con la variable EXECUTIONS_MODE=queue y conecta Redis como broker. La instancia main mantiene la definición de workflows, los triggers programados, y la API de administración. Los workers son pods adicionales que escuchan la cola y ejecutan los jobs. Los webhook pods son instancias ligeras que solo reciben HTTP y publican en Redis.
Esta separación te da tres beneficios inmediatos:
- Escalás workers independientemente de main. Si un workflow pesado satura los workers, agregas más réplicas sin tocar la API.
- Los webhooks nunca compiten con ejecuciones. Aunque haya un workflow corriendo 10 minutos, el endpoint sigue respondiendo en menos de 50ms.
- puedes hacer rolling restart de main sin perder ejecuciones en vuelo, porque los workers siguen procesando la cola.
El Helm chart oficial de n8n (en n8n-io/n8n-hosting) viene con esta separación lista. El chart de comunidad también, pero con diferencias en el modelo de persistencia que menciono más adelante.
El chart oficial vs el de comunidad
El equipo de Fairwinds empezó con el community chart porque era el único que soportaba queue mode y StatefulSet para el pod main. Lo corrieron un año. Después migraron al chart oficial porque el mantenimiento del community se volvió difícil de sostener.
La diferencia crítica está en cómo manejan la persistencia del pod main:
| Aspecto | Community chart | Chart oficial |
|---|---|---|
| Tipo de workload main | StatefulSet | Deployment |
| Persistencia | Un PVC por réplica (RWO) | Un PVC compartido (RWX) |
| Multi-attach en EBS gp3 | Funciona (RWO por nodo) | Falla (RWO no se comparte entre nodos) |
| Storage class requerida | Estándar | RWX (EFS, NFS, Longhorn, CephFS) |
Si corres en EKS con EBS gp3, el chart oficial te obliga a cambiar a EFS o a usar un storage class con provisioner RWX. En un homelab con Longhorn o CephFS ya tienes RWX, así que el chart oficial funciona out-of-the-box. En un VPS con docker-compose, esta discusión no aplica porque no usas Helm.
Setup mínimo con k3s para homelab
Si quieres un entorno de testing antes de tocar producción, k3s en una VM con 4GB de RAM es suficiente para validar la arquitectura completa. El chart oficial de n8n funciona out-of-the-box con el storage class local-path que k3s trae por defecto:
# Instalar k3s en una VM limpia
curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644
# Verificar que el storage class local-path está activo
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY
# local-path (default) rancher.io/local-path Delete
# Instalar External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets
--namespace external-secrets --create-namespace
# Instalar n8n con el chart oficial
helm repo add n8n https://n8n-io.github.io/n8n-hosting
helm install n8n n8n/n8n
--namespace n8n --create-namespace
--set persistence.enabled=false
--set queueMode.enabled=true
--set postgresql.enabled=true
--set redis.enabled=true
El flag persistence.enabled=false desactiva el PVC del pod main. Los charts inician PostgreSQL y Redis como subcharts para que tengas todo en un namespace, pero en producción real quieres esas dependencias externas (RDS, ElastiCache, etc.) por temas de backup, monitoring y SLA.
Persistencia: qué necesita n8n realmente en disco
Antes de migrar de un chart al otro, el equipo de Fairwinds hizo algo que todos deberíamos hacer: revisaron qué se guardaba efectivamente en el volumen del pod main. La respuesta corta: nada crítico.
n8n separa el estado en cuatro lugares:
- PostgreSQL: definiciones de workflows, credenciales encriptadas, historial de ejecuciones, schedules.
- Redis: cola de jobs en vuelo, locks de workflows concurrentes, caché de credenciales.
- Object storage (S3/MinIO): archivos binarios grandes que pasan por los nodos (PDFs, imágenes, attachments de email).
- Secrets manager externo: la
N8N_ENCRYPTION_KEYy los secretos de aplicación.
Con esas cuatro piezas bien configuradas, puedes recrear el pod main desde cero y se reconecta sin perder nada. La implicación práctica: desactiva la persistencia del pod main en el chart oficial. No necesitas un PVC.
Cómo configurar el object storage
Para n8n 1.0+, las variables de entorno que activan S3-compatible storage son:
N8N_EXECUTIONS_DATA_PRUNE=true
N8N_EXECUTIONS_DATA_MAX_AGE=168
N8N_BINARY_DATA_MODE=s3
N8N_BINARY_DATA_S3_BUCKET=n8n-binary-data
N8N_BINARY_DATA_S3_REGION=us-east-1
N8N_BINARY_DATA_S3_ACCESS_KEY=<access-key>
N8N_BINARY_DATA_S3_SECRET_KEY=<secret-key>
N8N_BINARY_DATA_S3_ENDPOINT=https://minio.ejemplo.local
Si usas MinIO local en lugar de S3, el ENDPOINT apunta al servicio interno de MinIO. Esto te permite mantener los binarios dentro de tu infraestructura, importante para compliance o para que no te cobren egress.
Secret management con External Secrets Operator
Aquí es donde la mayoría de los deploys caseros fallan. Ponen los secretos en Kubernetes Secrets planos y los commitean por error, o los rotan y descubren que la N8N_ENCRYPTION_KEY no se puede cambiar sin re-importar todas las credenciales.
La solución estándar es External Secrets Operator (ESO). ESO sincroniza secretos desde un vault externo (AWS Secrets Manager, Vault, GCP Secret Manager) hacia Kubernetes Secrets que n8n consume. La rotación se hace en el vault y ESO actualiza el Secret automáticamente.
El manifiesto mínimo es:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: n8n-secrets
namespace: n8n
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: n8n-secrets
data:
- secretKey: N8N_ENCRYPTION_KEY
remoteRef:
key: n8n/prod
property: encryption_key
- secretKey: DB_POSTGRESDB_PASSWORD
remoteRef:
key: n8n/prod
property: db_password
En el Helm values de n8n referenciás este Secret con existingSecret: n8n-secrets y todas las variables se montan automáticamente.
Estrategia de migración: cutover completo, no in-place
El error más caro que puedes cometer es asumir que un helm upgrade con el nuevo chart funciona. No funciona si cambia el modelo de persistencia. Lo que termina pasando es que el pod nuevo no levanta, los workers viejos todavía procesan jobs, y queda un estado mixto que es difícil de depurar.
Verificar la cola Redis antes del cutover
Antes de hacer el cambio de chart, asegurate de que la cola Redis está vacía o casi vacía. Si hay jobs en vuelo, esos workers viejos los van a terminar, pero cualquier nuevo job que entre durante el cutover necesita que el nuevo deployment ya esté listo para consumirlo:
# Conectarse a Redis y ver cuántos jobs hay pendientes
redis-cli -h redis.n8n.svc.cluster.local LLEN bull:default:wait
redis-cli -h redis.n8n.svc.cluster.local LLEN bull:default:active
redis-cli -h redis.n8n.svc.cluster.local LLEN bull:default:delayed
# Si los números son altos (>100), espera a que bajen
# o pausa los triggers en n8n antes del cutover
Durante un cutover controlado puedes pausar todos los triggers con PUT /api/v1/workflows?active=false en la API de n8n, hacer el cambio, y reactivarlos después. Si usas triggers de webhook, pausarlos no afecta las URLs entrantes, pero sí evita que se disparen workflows programados.
El patrón correcto es cutover completo:
- Testea el rollback en no-producción. Despliega el chart nuevo en un namespace separado, apuntando a una copia de la base, y verifica que los workflows conocidos corren. Después simula el rollback.
- Snapshot de la base de datos.
pg_dumpo el snapshot automatizado de tu proveedor. Guarda el archivo en un lugar que sobreviva al cluster. - No cambies la versión de n8n en el cutover. Si la versión que tienes es 1.42.1, mantén esa misma versión con el chart nuevo. Los upgrades de versión se hacen después, con su propia ventana de mantenimiento.
- Mantené PostgreSQL, Redis, S3, IAM y secrets en su lugar. Solo cambiás el chart y recreás los pods.
- verifica en producción los endpoints críticos. El dashboard, el endpoint de health, un workflow de prueba, y un webhook público conocido.
Este flujo suena conservador, pero el costo de un cutover mal hecho (corrupción de base, pérdida de workflows, secretos huerfanos) es órdenes de magnitud mayor que las dos horas de downtime controlado.
Ingress y separación de tráfico
Una decisión de diseño que te salva en producción: separar ingress por tipo de tráfico. Los webhooks públicos van detrás de un load balancer con IP pública y certificado TLS dedicado. Los endpoints internos (API, dashboard, REST API de admin) van en un hostname privado con autenticación adicional.
El chart oficial define dos ingress separados:
ingress:
main:
enabled: true
hostname: n8n.ejemplo.local
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: n8n-admin-auth
webhook:
enabled: true
hostname: webhooks.ejemplo.com
tls:
- secretName: n8n-webhook-tls
hosts:
- webhooks.ejemplo.com
El webhook ingress no debe tener autenticación porque los clientes externos no pueden manejar Basic Auth. La seguridad la ponés en el nivel de los nodos: rate limiting con nginx.ingress.kubernetes.io/limit-rps: "20", IP allowlist si tu audiencia es conocida, y WAF si usas Cloudflare frente al cluster.
Troubleshooting: los tres problemas recurrentes
Después de un año corriendo n8n en producción, los mismos tres síntomas aparecen una y otra vez. Los listo con la causa raíz y la solución rápida.
Cómo migrar de SQLite a PostgreSQL
Si vienes de la versión demo, tarde o temprano vas a necesitar migrar. El comando oficial preserva todas las tablas, credenciales encriptadas, e historial de ejecuciones:
# 1. Dump desde SQLite
docker exec n8n-demo n8n export:workflow --all --output=/tmp/workflows.json
docker exec n8n-demo n8n export:credentials --all --output=/tmp/credentials.json
# 2. Levantar PostgreSQL (15+ recomendado)
docker run -d --name n8n-postgres
-e POSTGRES_USER=n8n
-e POSTGRES_PASSWORD=$(openssl rand -hex 24)
-e POSTGRES_DB=n8n
-v n8n-pgdata:/var/lib/postgresql/data
postgres:15-alpine
# 3. Levantar n8n apuntando a Postgres
docker run -d --name n8n-prod
-e DB_TYPE=postgresdb
-e DB_POSTGRESDB_HOST=n8n-postgres
-e DB_POSTGRESDB_DATABASE=n8n
-e DB_POSTGRESDB_USER=n8n
-e DB_POSTGRESDB_PASSWORD=<same-password>
n8nio/n8n:1.42.1
# 4. Importar workflows y credenciales
docker exec n8n-prod n8n import:workflow --input=/tmp/workflows.json
docker exec n8n-prod n8n import:credentials --input=/tmp/credentials.json
El paso 4 es delicado: las credenciales importadas mantienen la encriptación con la N8N_ENCRYPTION_KEY original, así que tienes que setear la misma key en la instancia nueva con -e N8N_ENCRYPTION_KEY=<misma-key>. Si la pierdes, todas las credenciales quedan ilegibles.
Webhooks que devuelven 504
Síntoma: el cliente externo reporta timeout, pero el workflow sí se ejecuta. Causa: el ingress está esperando respuesta del workflow, y el workflow tarda más que el timeout del ingress. Solución: configurar el ingress para que devuelva 202 inmediatamente y deje el webhook en cola. En nginx ingress, nginx.ingress.kubernetes.io/proxy-read-timeout: "60" y un nodo webhook dedicado que solo publique en Redis sin esperar.
Workers que se quedan en estado Pending
Síntoma: kubectl get pods muestra workers que no levantan. Causa: Redis no responde o las credenciales cambiaron. Solución: kubectl logs -l app=n8n-worker --tail=50 para ver el error. Si es “ECONNREFUSED” a Redis, el Secret QUEUE_BULL_REDIS_PASSWORD tiene la contraseña vieja. Rotar el Secret en el vault y esperar el refreshInterval de ESO.
Ejecuciones que se duplican
Síntoma: el mismo workflow corre dos veces con el mismo trigger. Causa: Redis perdió la lock de concurrencia porque el worker murió antes de hacer ACK. Solución: bajar el TTL de la lock a 30 segundos y agregar un nodo “Execute Once” al inicio del workflow que verifique el ID de ejecución. Para prevenir: configurar QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD=30000 y EXECUTIONS_TIMEOUT=300.
¿Vale la pena correr n8n en producción?
Depende del volumen. Si corres menos de 1,000 ejecuciones por día, un n8n single-container con PostgreSQL (no SQLite) y backups diarios te da por años sin entrar al modo queue. Si pasas las 10,000 ejecuciones por día, o si los webhooks son críticos para tu negocio, entonces sí, la inversión en queue mode + workers + chart oficial se paga sola.
Para homelab y proyectos personales, n8n sigue siendo la mejor opción. La complejidad del modo queue solo vale la pena cuando tienes un SLA. Si tu n8n se cae y nadie pierde dinero, deja el single-container y dedica tu tiempo a otra cosa.
Cómo decidir si subir a queue mode
El indicador más claro de que necesitas queue mode es ver la cola Redis crecer. Si no usas Redis porque estás en single-container, el indicador es ver el dashboard lento cuando hay workflows corriendo. Antes de tomar la decisión, monitoreá durante una semana:
# Métricas clave de una instancia single-container
docker exec n8n-prod n8n execute
--command "SELECT count(*) FROM execution_entity WHERE finished=false"
# Si este número promedia >5, hay workflows colgados o workers lentos
# Combinado con CPU del container >70%, es hora de queue mode
# Para ver uso de CPU/memoria del container
docker stats n8n-prod --no-stream
La transición de single-container a queue mode es un proyecto de 2-3 días para un sysadmin con experiencia, no una mejora de un fin de semana. Planificá tiempo para probar el cutover y para resolver las queries SQL que aparecen cuando mueves millones de filas de execution_entity a una base nueva.
Preguntas frecuentes
¿Puedo migrar de SQLite a PostgreSQL sin perder workflows?
Sí. n8n incluye el comando n8n db:sqlite2pg que copia todas las tablas de una base SQLite a PostgreSQL en vivo. haz un backup antes, detené la instancia, corré la migración, y levantá la nueva instancia apuntando a PostgreSQL. Las credenciales encriptadas se mantienen porque dependen de la N8N_ENCRYPTION_KEY, no del motor de base de datos.
¿Cuántos workers necesito para N ejecuciones por día?
Regla práctica: 1 worker por cada 500 ejecuciones cortas (<30s) por hora pico. Si tus workflows son pesados (más de 2 minutos), 1 worker por cada 100 ejecuciones por hora. Empezá con 2 workers y un HPA configurado a 70% de CPU antes de dimensionar al alza. Sobre-dimensionar workers es caro en RAM; sub-dimensionarlos se ve en la cola Redis creciendo.
¿Qué pasa si pierdo la N8N_ENCRYPTION_KEY?
Perdés acceso a todas las credenciales encriptadas en los workflows, pero no pierdes los workflows en sí. n8n no puede descifrar las credenciales sin esa key, así que los workflows van a fallar al intentar usarlas. El recovery es re-crear las credenciales manualmente desde el editor y la nueva key. Por eso se guarda en Secrets Manager con backup, nunca solo en un ConfigMap del cluster.
¿Es seguro exponer webhooks de n8n públicamente?
Con caveats. Los webhooks son URLs aleatorias largas (32+ chars) que actúan como autenticación por obscurity. Eso es suficiente para tráfico de bajo riesgo (formularios públicos, integraciones no-críticas), pero no para tráfico donde alguien pueda iterar sobre URLs. Para webhooks críticos agregá un header X-Hub-Signature que n8n valida contra un secreto compartido con el cliente.
¿Cómo hago upgrade de n8n sin downtime?
Secuencia recomendada: (1) leé las release notes y breaking changes de la versión destino, (2) prueba en no-producción con una copia de la base, (3) programá el upgrade en horario de bajo tráfico, (4) haz helm upgrade con la nueva image tag, (5) verifica que el rollout completó con kubectl rollout status, (6) corré un workflow de prueba end-to-end. Si algo falla, helm rollback y la versión anterior levanta en segundos.
