Platform Engineering & AI Infrastructure

n8n + MCP on Kubernetes: Production-Grade Self-Hosted AI Agent Infrastructure

Complete guide to deploying n8n with Model Context Protocol servers on Kubernetes using Helm, ArgoCD GitOps, ingress, persistence, and OAuth — the exact stack running n8n.tca-infraforge.site.

By Temitayo Charles Akinniranye 18 min read

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 PlaneManaged$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 / ALBModerate$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.

Book Discovery Call → $5K Fixed Scope

Stay Updated

Get weekly insights on DevOps, AI automation, and technical playbooks. No spam, unsubscribe anytime.