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.tayoca.com.

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.tayoca.com                      │
│  ┌──────────────────┐  ┌──────────────────┐  ┌────────────────┐  │
│  │   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.tayoca.com"
    - name: N8N_PROTOCOL
      value: "https"
    - name: N8N_PORT
      value: "5678"
    - name: WEBHOOK_URL
      value: "https://n8n.tayoca.com"
    - 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.tayoca.com
        paths:
          - path: /
            pathType: Prefix
    tls:
      - secretName: n8n-tls
        hosts:
          - n8n.tayoca.com

  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.tayoca.com/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.