Servidores

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:

  1. 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.
  2. 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.
  3. Los secretos viven en el filesystem del pod. Una variable de entorno N8N_ENCRYPTION_KEY guardada 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_KEY y 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:

  1. 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.
  2. Snapshot de la base de datos. pg_dump o el snapshot automatizado de tu proveedor. Guarda el archivo en un lugar que sobreviva al cluster.
  3. 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.
  4. Mantené PostgreSQL, Redis, S3, IAM y secrets en su lugar. Solo cambiás el chart y recreás los pods.
  5. 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.

Lee también

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux para empresas y proyectos personales. Me especializo en infraestructura, redes, virtualización, monitoring, automation y self-hosting. Sobre SoyLinux.com SoyLinux.com es un blog técnico en español sobre Linux, software libre y self-hosting. Lo mantengo en mi tiempo libre. Áreas de expertise - Linux server administration: RHEL, CentOS, Rocky, Alma, Fedora, Debian, Ubuntu - Network services: Postfix, BIND, Samba, NFS, Squid - Virtualización y containers: KVM, Proxmox, Docker, Podman - Monitoring: Prometheus, Grafana, Zabbix - Automation: Ansible, Bash scripting - Self-hosting: Vaultwarden, Nextcloud, Jellyfin, Immich, Homelab Contacto - Email: [email protected]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *