Deploy n8n on Kubernetes: Manifests and Setup
Table of Contents
n8n Deployment: Kubernetes Manifest Guide
Part 2 of 3. Part 1: Docker Compose Setup | Part 3: Configuration Deep Dive
Kubernetes Deployment
When I move n8n into a production cluster, I use Kubernetes with Helm-like manifests managed through GitOps. The following manifests assume you have a running cluster with an ingress controller and cert-manager for TLS.
Step 1: Namespace and Secrets
apiVersion: v1kind: Namespacemetadata: name: n8napiVersion: v1kind: Secretmetadata: name: n8n-secrets namespace: n8ntype: OpaquestringData: postgres-password: "<GENERATE_32_CHAR_PASSWORD>" encryption-key: "<GENERATE_32_CHAR_KEY>" basic-auth-user: "admin" basic-auth-password: "<GENERATE_24_CHAR_PASSWORD>"Apply with kubectl apply -f namespace.yaml -f secret.yaml.
Step 2: PostgreSQL StatefulSet
apiVersion: apps/v1kind: StatefulSetmetadata: name: postgres namespace: n8nspec: serviceName: postgres replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:16-alpine env: - name: POSTGRES_USER value: "n8n" - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: n8n-secrets key: postgres-password - name: POSTGRES_DB value: "n8n" ports: - containerPort: 5432 volumeMounts: - name: postgres-data mountPath: /var/lib/postgresql/data resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "1Gi" cpu: "1000m" volumeClaimTemplates: - metadata: name: postgres-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi---apiVersion: v1kind: Servicemetadata: name: postgres namespace: n8nspec: selector: app: postgres ports: - port: 5432 targetPort: 5432Step 3: Redis Deployment
apiVersion: apps/v1kind: Deploymentmetadata: name: redis namespace: n8nspec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7-alpine command: - redis-server - --appendonly - "yes" - --maxmemory - "256mb" - --maxmemory-policy - "allkeys-lru" ports: - containerPort: 6379 volumeMounts: - name: redis-data mountPath: /data resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" volumes: - name: redis-data emptyDir: {}---apiVersion: v1kind: Servicemetadata: name: redis namespace: n8nspec: selector: app: redis ports: - port: 6379 targetPort: 6379Step 4: n8n Main Deployment
apiVersion: apps/v1kind: Deploymentmetadata: name: n8n-main namespace: n8nspec: replicas: 1 selector: matchLabels: app: n8n-main template: metadata: labels: app: n8n-main spec: containers: - name: n8n image: n8nio/n8n:1.50 ports: - containerPort: 5678 env: - name: DB_TYPE value: "postgresdb" - name: DB_POSTGRESDB_HOST value: "postgres" - name: DB_POSTGRESDB_PORT value: "5432" - name: DB_POSTGRESDB_DATABASE value: "n8n" - name: DB_POSTGRESDB_USER value: "n8n" - name: DB_POSTGRESDB_PASSWORD valueFrom: secretKeyRef: name: n8n-secrets key: postgres-password - name: EXECUTIONS_MODE value: "queue" - name: QUEUE_BULL_REDIS_HOST value: "redis" - name: QUEUE_BULL_REDIS_PORT value: "6379" - name: N8N_ENCRYPTION_KEY valueFrom: secretKeyRef: name: n8n-secrets key: encryption-key - name: WEBHOOK_URL value: "https://n8n.yourdomain.com/" - name: GENERIC_TIMEZONE value: "UTC" - name: N8N_BASIC_AUTH_ACTIVE value: "true" - name: N8N_BASIC_AUTH_USER valueFrom: secretKeyRef: name: n8n-secrets key: basic-auth-user - name: N8N_BASIC_AUTH_PASSWORD valueFrom: secretKeyRef: name: n8n-secrets key: basic-auth-password resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "2Gi" cpu: "2000m"---apiVersion: v1kind: Servicemetadata: name: n8n-main namespace: n8nspec: selector: app: n8n-main ports: - port: 5678 targetPort: 5678Step 5: n8n Workers
apiVersion: apps/v1kind: Deploymentmetadata: name: n8n-worker namespace: n8nspec: replicas: 3 selector: matchLabels: app: n8n-worker template: metadata: labels: app: n8n-worker spec: containers: - name: n8n image: n8nio/n8n:1.50 command: ["n8n", "worker"] env: - name: DB_TYPE value: "postgresdb" - name: DB_POSTGRESDB_HOST value: "postgres" - name: DB_POSTGRESDB_PASSWORD valueFrom: secretKeyRef: name: n8n-secrets key: postgres-password - name: QUEUE_BULL_REDIS_HOST value: "redis" - name: QUEUE_BULL_REDIS_PORT value: "6379" - name: N8N_ENCRYPTION_KEY valueFrom: secretKeyRef: name: n8n-secrets key: encryption-key - name: GENERIC_TIMEZONE value: "UTC" resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "1Gi" cpu: "1000m"Step 6: Ingress with TLS
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: n8n namespace: n8n annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/ssl-redirect: "true"spec: ingressClassName: nginx tls: - hosts: - n8n.yourdomain.com secretName: n8n-tls rules: - host: n8n.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: n8n-main port: number: 5678Apply all manifests:
kubectl apply -f namespace.yaml -f secret.yaml -f postgres.yaml -f redis.yaml -f n8n-main.yaml -f n8n-worker.yaml -f ingress.yamlVerify the rollout:
kubectl get pods -n n8nkubectl logs -n n8n deployment/n8n-main | grep "Editor is now accessible"Webhook Security
Webhooks are the most exposed surface of any automation platform. Here is how I lock them down.
Use a Dedicated Webhook Subdomain
I always separate the webhook ingress from the editor UI. If your main instance is at n8n.yourdomain.com, run webhooks on hooks.yourdomain.com. This lets you apply different rate limits, WAF rules, and authentication policies.
Enable Webhook Authentication
n8n supports basic auth and header-based auth on webhook workflows. For production webhooks, I add a custom header check:
// n8n Webhook node → Headers{ "X-Webhook-Secret": "{{$env.WEBHOOK_SECRET}}"}Store WEBHOOK_SECRET in the n8n credentials vault, not in the workflow JSON.
IP Allowlisting
If your webhooks only receive events from known sources (GitHub, Stripe, etc.), restrict ingress by source IP:
# nginx ingress annotationnginx.ingress.kubernetes.io/whitelist-source-range: "192.30.252.0/22, 185.199.108.0/22"Credential Management
n8n encrypts credentials at rest using the N8N_ENCRYPTION_KEY. However, there are additional layers you should add.
| Layer | Implementation | Risk Mitigated |
|---|---|---|
| Encryption at rest | N8N_ENCRYPTION_KEY | Database theft |
| Encryption in transit | TLS on ingress | Man-in-the-middle |
| Secret injection | Kubernetes Secrets / Docker Secrets | Credential leakage in env |
| Access control | n8n RBAC + basic auth | Unauthorized workflow edits |
| Audit logging | PostgreSQL query logs | Credential access tracing |
I rotate the encryption key quarterly. To do this, export all workflows, destroy the volume, start n8n with a new key, and re-import. It is tedious but necessary for high-security environments.
AI Agent Node Setup
n8n’s AI Agent node lets you build agentic workflows that can call tools, reason over context, and interact with LLMs. I use this to build self-healing infrastructure alerts and automated ticket triage.
Connecting to a Self-Hosted LLM
If you followed my Ollama deployment guide, you already have a local LLM running. Wire it into n8n like this:
- Add an HTTP Request node or use the Ollama Chat Model node (community node).
- Set the base URL to your Ollama instance:
http://ollama.ai-serving.svc.cluster.local:11434. - Select the model:
llama3.1:8bormistral:7b. - For the AI Agent node, set the System Message to define the agent’s role:
You are an SRE assistant. You have access to Kubernetes logs, deployment statuses,and alerting history. When asked about an incident, summarize the symptoms,suggest root causes, and recommend the next diagnostic step. Do not executecommands without explicit approval.Basic Workflow: Alert → LLM → Slack
Here is a simple workflow I run for triaging Prometheus alerts:
- Webhook Trigger: Receives alert payload from Alertmanager.
- HTTP Request: Fetches recent logs from Loki for the affected service.
- AI Agent: Summarizes logs and suggests a root cause.
- Slack: Posts the summary to
#incidentswith approve/reject buttons. - If Approved: A second branch runs a safe diagnostic (e.g.,
kubectl get events).
This keeps humans in the loop while automating the noise reduction that usually burns out on-call engineers.
FAQ
What is n8n and what makes it different from Zapier?
n8n is an open-source workflow automation platform. The key difference from Zapier: you self-host everything. Your data never touches third-party servers, credentials stay encrypted on your infrastructure, and there are no usage tiers limiting how many workflows you can run. I switched to n8n after Zapier changed their pricing model and my automation bill tripled overnight.
How do I scale n8n workers on Kubernetes?
Set the replicas field in the n8n-worker Deployment manifest. I start with 3 replicas and scale based on Redis queue depth. Monitor LLEN bull:jobs in Redis. When it stays above 100 for more than 5 minutes, I add more workers. Each worker needs at least 256Mi RAM and 250m CPU for basic workflows.
Can I use n8n with vLLM instead of Ollama?
Yes. Both Ollama and vLLM expose OpenAI-compatible APIs. Point the HTTP Request node or the OpenAI Chat Model node at your vLLM service URL and set the model name. I run Ollama for development and vLLM for production workloads.
How do I back up my n8n instance on Kubernetes?
Back up two things: the PostgreSQL database (pg_dump) and the encryption key. Without the N8N_ENCRYPTION_KEY, your credentials are unrecoverable. I run nightly pg_dumps to S3-compatible storage and store the encryption key in a secure vault.
What is queue mode and when do I need it?
Queue mode separates the web UI from workflow execution workers using Redis as a job queue. I enable queue mode as soon as I have more than 5 active workflows. Without it, a single slow workflow can block the entire execution pipeline.
Is the n8n AI Agent node production-ready?
For internal tooling and alert triage, yes. I would not trust it for autonomous remediation in production without human approval gates. Always keep a human in the loop for destructive actions like deleting resources or modifying deployments.
Parts in this series: Part 1: Docker Compose Setup ← | Part 3: Configuration Deep Dive →