n8n is the workflow automation backbone. MCP (Model Context Protocol) is the universal connector for AI agents. Kubernetes is the only platform that gives you multi-tenancy, scaling, and GitOps out of the box. This article shows how we wired them together in production — no managed SaaS, full control, zero vendor lock-in.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ n8n.tca-infraforge.site │
│ ┌──────────────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ n8n Main │ │ n8n Worker │ │ MCP Servers │ │
│ │ (Deployment) │ │ (Deployment) │ │ (Deployments) │ │
│ │ - Webhook ingress│ │ - Queue mode │ │ - GitHub │ │
│ │ - API + UI │ │ - Redis queue │ │ - GitLab │ │
│ │ - Postgres │ │ - Horizontal │ │ - Kubernetes │ │
│ └────────┬─────────┘ └────────┬─────────┘ │ - Cloudflare │ │
│ │ │ │ - PostgreSQL │ │
│ └──────────┬──────────┘ └───────┬────────┘ │
│ │ │ │
│ ┌──────────▼──────────┐ │ │
│ │ Redis Cluster │◀──────────────────┘ │
│ │ (Queue + Cache) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ PostgreSQL HA │ │
│ │ (CloudNativePG) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Prerequisites
- Kubernetes cluster (k3s, EKS, GKE, or kind for dev)
- Helm 3.12+, kubectl, argocd CLI
- Domain with Cloudflare DNS (for TLS via cert-manager)
- PostgreSQL operator (CloudNativePG) or managed PG
- Redis operator or managed Redis (ElastiCache, Cloud Memorystore)
Step 1: Namespace & Core Operators
# Create namespace
kubectl create namespace n8n
# Install CloudNativePG for PostgreSQL HA
kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/release/kustomize/crds.yaml
kubectl apply -k https://github.com/cloudnative-pg/cloudnative-pg/kustomize/install
# Install Redis Operator (or use managed)
kubectl apply -k https://github.com/spotahome/redis-operator/kustomize/overlays/stable
Step 2: PostgreSQL Cluster (CloudNativePG)
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: n8n-postgres
namespace: n8n
spec:
instances: 3
primaryUpdateStrategy: unsupervised
postgresql:
parameters:
max_connections: "200"
shared_buffers: "256MB"
bootstrap:
initdb:
database: n8n
owner: n8n
secret:
name: n8n-postgres-credentials
storage:
size: 20Gi
storageClass: fast-ssd
backup:
barmanObjectStore:
destinationPath: s3://n8n-backups/postgres
s3Credentials:
accessKeyId:
name: n8n-backup-aws
key: ACCESS_KEY_ID
secretAccessKey:
name: n8n-backup-aws
key: SECRET_ACCESS_KEY
retentionPolicy: "30d"
schedule: "0 2 * * *"
Step 3: Redis for Queue Mode
n8n's queue mode requires Redis for workflow execution distribution across workers.
apiVersion: redis.spotahome.com/v1
kind: Redis
metadata:
name: n8n-redis
namespace: n8n
spec:
replicas: 3
redisVersion: "7.2"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
storage:
size: 5Gi
storageClass: fast-ssd
sentinel:
enabled: true
replicas: 3
Step 4: n8n Helm Values (Production-Grade)
# values-n8n.yaml
global:
image:
repository: n8nio/n8n
tag: "1.47.0"
pullPolicy: IfNotPresent
n8n:
replicaCount: 1
env:
- name: N8N_HOST
value: "n8n.tca-infraforge.site"
- name: N8N_PROTOCOL
value: "https"
- name: N8N_PORT
value: "5678"
- name: WEBHOOK_URL
value: "https://n8n.tca-infraforge.site"
- name: DB_TYPE
value: "postgresdb"
- name: DB_POSTGRESDB_HOST
value: "n8n-postgres-rw.n8n.svc.cluster.local"
- 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-postgres-credentials
key: password
- name: N8N_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: n8n-encryption-key
key: key
- name: EXECUTIONS_MODE
value: "queue"
- name: QUEUE_BULL_REDIS_HOST
value: "n8n-redis.n8n.svc.cluster.local"
- name: QUEUE_BULL_REDIS_PORT
value: "6379"
- name: QUEUE_BULL_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: n8n-redis
key: redis-password
- name: N8N_LOG_LEVEL
value: "info"
- name: N8N_METRICS
value: "true"
- name: N8N_METRICS_PREFIX
value: "n8n"
- name: GENERIC_TIMEZONE
value: "America/Toronto"
# OAuth credentials (stored in secrets)
- name: N8N_OAUTH2_CLIENT_ID_GITHUB
valueFrom:
secretKeyRef:
name: n8n-oauth
key: github-client-id
- name: N8N_OAUTH2_CLIENT_SECRET_GITHUB
valueFrom:
secretKeyRef:
name: n8n-oauth
key: github-client-secret
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
hosts:
- host: n8n.tca-infraforge.site
paths:
- path: /
pathType: Prefix
tls:
- secretName: n8n-tls
hosts:
- n8n.tca-infraforge.site
persistence:
enabled: true
size: 10Gi
storageClass: fast-ssd
accessMode: ReadWriteOnce
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /healthz
port: 5678
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /healthz
port: 5678
initialDelaySeconds: 10
periodSeconds: 10
worker:
enabled: true
replicaCount: 3
env:
- name: EXECUTIONS_MODE
value: "queue"
- name: QUEUE_BULL_REDIS_HOST
value: "n8n-redis.n8n.svc.cluster.local"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
Step 5: MCP Servers as Sidecars / Separate Deployments
Each MCP server runs as its own Deployment, exposing stdio over HTTP via mcp-proxy or native HTTP transport.
Example: GitHub MCP Server
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-github
namespace: n8n
spec:
replicas: 2
selector:
matchLabels:
app: mcp-github
template:
metadata:
labels:
app: mcp-github
spec:
containers:
- name: mcp-github
image: ghcr.io/modelcontextprotocol/server-github:latest
env:
- name: GITHUB_PERSONAL_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: mcp-github-credentials
key: token
- name: GITHUB_DYNAMIC_TOOLSETS
value: "true"
ports:
- containerPort: 3000
name: http
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /health
port: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
---
apiVersion: v1
kind: Service
metadata:
name: mcp-github
namespace: n8n
spec:
selector:
app: mcp-github
ports:
- port: 3000
targetPort: 3000
n8n MCP Configuration (via n8n UI or config map)
# In n8n: Settings → Community Nodes → Add MCP Server
# Server URL: http://mcp-github.n8n.svc.cluster.local:3000
# Transport: HTTP (SSE)
# Authentication: None (internal network)
Step 6: GitOps with ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: n8n-stack
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/temitayocharles/homelab-gitops
targetRevision: main
path: argocd/applications/n8n
helm:
valueFiles:
- values-n8n.yaml
- values-postgres.yaml
- values-redis.yaml
- values-mcp-servers.yaml
destination:
server: https://kubernetes.default.svc
namespace: n8n
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Step 7: Secrets Management (External Secrets Operator)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: n8n-secrets
namespace: n8n
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: n8n-secrets
creationPolicy: Owner
data:
- secretKey: encryption-key
remoteRef:
key: n8n/encryption-key
property: key
- secretKey: github-client-id
remoteRef:
key: n8n/oauth
property: github-client-id
- secretKey: github-client-secret
remoteRef:
key: n8n/oauth
property: github-client-secret
- secretKey: cloudflare-api-token
remoteRef:
key: n8n/integrations
property: cloudflare-api-token
Step 8: Monitoring & Alerting
# ServiceMonitor for Prometheus
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: n8n
namespace: n8n
spec:
selector:
matchLabels:
app: n8n
endpoints:
- port: http
path: /metrics
interval: 30s
---
# Key alerts (PrometheusRule)
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: n8n-alerts
namespace: n8n
spec:
groups:
- name: n8n.rules
rules:
- alert: N8NWorkflowFailureRate
expr: rate(n8n_workflow_executions_failed_total[5m]) / rate(n8n_workflow_executions_total[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "n8n workflow failure rate > 5%"
- alert: N8NQueueLag
expr: n8n_queue_jobs_waiting > 100
for: 10m
labels:
severity: warning
annotations:
summary: "n8n queue has {{ $value }} waiting jobs"
- alert: N8NWorkerDown
expr: up{job="n8n-worker"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "n8n worker pod down"
Step 9: OAuth Configuration (GitHub, GitLab, Google, etc.)
Create OAuth apps in each provider with callback URL: https://n8n.tca-infraforge.site/oauth2/callback
Store credentials in Vault → External Secrets → n8n env vars (as shown in values-n8n.yaml).
Common Operational Tasks
| Task | Command |
|---|---|
| View n8n logs | kubectl logs -n n8n -l app=n8n -f |
| Scale workers manually | kubectl scale deployment n8n-worker -n n8n --replicas=5 |
| Backup workflows | kubectl exec -n n8n deploy/n8n -- n8n export:workflow --all --output=/tmp/workflows.json |
| Update n8n version | Update tag in values-n8n.yaml → ArgoCD sync |
| Add MCP server | Deploy new Deployment+Service → Register in n8n UI |
Cost Breakdown (Monthly, AWS us-east-1, 3-node cluster)
| Component | Spec | Est. Cost |
|---|---|---|
| EKS Control Plane | Managed | $73 |
| Worker Nodes (3× m6i.xlarge) | 4 vCPU, 16 GiB each | $380 |
| RDS PostgreSQL (db.r6g.xlarge, Multi-AZ) | 4 vCPU, 32 GiB | $350 |
| ElastiCache Redis (cache.r6g.xlarge) | 4 vCPU, 32 GiB | $220 |
| EBS Volumes (GP3) | ~100 GiB total | $8 |
| Data Transfer / ALB | Moderate | $30 |
| Total | ~$1,061/mo |
vs. n8n Cloud Pro: $120/mo (10K executions) — self-hosted wins at scale + full data control + custom MCP servers.
Troubleshooting Quick Reference
- Webhooks not firing: Check ingress annotations (proxy-body-size, timeouts), verify Cloudflare proxy DNS
- Workers stuck: Redis connectivity, check
QUEUE_BULL_REDIS_*env vars - MCP server not appearing: Verify Service name matches n8n config, check mcp-proxy health endpoint
- OAuth redirect mismatch: Exact callback URL in provider console, check
N8N_HOST/WEBHOOK_URL - Postgres connection pool exhaustion: Increase
max_connections, add PgBouncer
Next Steps
- Add Temporal for durable execution / long-running workflows
- Deploy n8n-mcp custom nodes for tighter integration
- Implement multi-tenancy with n8n's project feature + namespace isolation
- Build custom MCP servers for your internal APIs (see Sivanta platform)
Want This Running for Your Team?
I offer a "n8n + MCP on Kubernetes" implementation package — 2-week engagement, GitOps repo, monitoring, docs, and knowledge transfer.