Deploy n8n di Kubernetes: dari Nol Sampai Jalan
Deploy n8n di Kubernetes: dari Nol Sampai Jalan
Saya pertama kali nyoba pasang n8n di Docker Compose. Gampang, lima menit kelar. Masalahnya begitu traffic workflow mulai naik dan tim mulai pakai bareng, Compose terasa kurang — restart manual, tidak ada HA, scaling repot.
Kubernetes fix semua itu, tapi butuh setup lebih. Artikel ini saya tulis runut dari awal, termasuk bagian yang sering bikin orang nyangkut.
Prasyarat
Pastikan sudah punya:
- Kubernetes cluster yang jalan (bisa lokal pakai
kindatauminikube, atau cloud: GKE, EKS, AKS) kubectlterkonfigurasi dan bisa konek ke cluster- Helm v3 (opsional tapi sangat memudahkan)
- Persistent Volume provisioner — di cloud biasanya sudah ada, di lokal perlu setup manual
Cek dulu semuanya sebelum lanjut:
kubectl version --short
helm version
kubectl get nodesKalau ada yang error di sini, bereskan dulu. Lanjut tanpa fondasi yang solid itu resep sakit kepala.
Arsitektur yang Akan Kita Buat
Sebelum langsung copy-paste YAML, penting tahu dulu gambaran besarnya.
[Ingress / LoadBalancer]
│
[n8n Service]
│
[n8n Deployment]
(1 atau lebih Pod)
│
[PostgreSQL Service]
│
[PostgreSQL StatefulSet]
│
[PersistentVolumeClaim]n8n butuh database untuk menyimpan credentials, workflows, dan execution history. SQLite bisa dipakai, tapi tidak cocok untuk multi-replica karena file-based. Kita pakai PostgreSQL.
Step 1 — Buat Namespace
Pisahkan n8n di namespace sendiri supaya tidak campur aduk dengan workload lain.
kubectl create namespace n8nSemua resource di sini nanti pakai flag --namespace n8n atau di-set lewat metadata.namespace.
Step 2 — Simpan Credentials di Secret
Jangan hardcode password di YAML. Pakai Secret.
# secret.yaml apiVersion: v1
kind: Secret
metadata: name: n8n-secret
namespace: n8n
type: Opaque
stringData: db-password: "ganti-dengan-password-kuat" n8n-encryption-key: "random-string-32-karakter-minimal" n8n-user-email: "admin@domain.com" n8n-user-password: "password-untuk-login-n8n"Untuk generate encryption key yang aman:
openssl rand -hex 24Apply:
kubectl apply -f secret.yamlStep 3 — Deploy PostgreSQL
Kita pakai StatefulSet supaya pod mendapat identity yang stabil dan storage-nya persistent.
# postgres.yaml apiVersion: v1
kind: Service
metadata: name: postgres
namespace: n8n
spec: selector: app: postgres
ports: - port: 5432 targetPort: 5432 clusterIP: None # Headless service untuk StatefulSet --- apiVersion: apps/v1
kind: StatefulSet
metadata: name: postgres
namespace: n8n
spec: serviceName: postgres
replicas: 1 selector: matchLabels: app: postgres
template: metadata: labels: app: postgres
spec: containers: - name: postgres
image: postgres:15-alpine
ports: - containerPort: 5432 env: - name: POSTGRES_DB
value: n8n
- name: POSTGRES_USER
value: n8n
- name: POSTGRES_PASSWORD
valueFrom: secretKeyRef: name: n8n-secret
key: db-password
volumeMounts: - name: postgres-data
mountPath: /var/lib/postgresql/data
resources: requests: cpu: 100m
memory: 256Mi
limits: cpu: 500m
memory: 512Mi
volumeClaimTemplates: - metadata: name: postgres-data
spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gikubectl apply -f postgres.yamlTunggu sampai pod PostgreSQL Running:
kubectl get pods -n n8n -wKalau stuck di Pending, kemungkinan masalah PersistentVolume — cek dengan:
kubectl describe pod postgres-0 -n n8nStep 4 — Deploy n8n
Ini bagian intinya. Kita pakai Deployment untuk n8n karena stateless (state ada di database, bukan di pod).
# n8n-deployment.yaml apiVersion: v1
kind: Service
metadata: name: n8n
namespace: n8n
spec: selector: app: n8n
ports: - port: 80 targetPort: 5678 type: ClusterIP
--- apiVersion: apps/v1
kind: Deployment
metadata: name: n8n
namespace: n8n
spec: replicas: 1 selector: matchLabels: app: n8n
template: metadata: labels: app: n8n
spec: containers: - name: n8n
image: n8nio/n8n:latest
ports: - containerPort: 5678 env: # Database - name: DB_TYPE
value: postgresdb
- name: DB_POSTGRESDB_HOST
value: postgres.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-secret
key: db-password
# n8n Config - name: N8N_ENCRYPTION_KEY
valueFrom: secretKeyRef: name: n8n-secret
key: n8n-encryption-key
- name: N8N_HOST
value: "n8n.domain.com" # ganti dengan domain kamu - name: N8N_PROTOCOL
value: "https" - name: WEBHOOK_URL
value: "https://n8n.domain.com/" # Auth awal (opsional, bisa pakai SSO nanti) - name: N8N_BASIC_AUTH_ACTIVE
value: "true" - name: N8N_BASIC_AUTH_USER
valueFrom: secretKeyRef: name: n8n-secret
key: n8n-user-email
- name: N8N_BASIC_AUTH_PASSWORD
valueFrom: secretKeyRef: name: n8n-secret
key: n8n-user-password
# Timezone - name: GENERIC_TIMEZONE
value: Asia/Jakarta
volumeMounts: - name: n8n-data
mountPath: /home/node/.n8n
resources: requests: cpu: 200m
memory: 256Mi
limits: cpu: 1000m
memory: 1Gi
livenessProbe: httpGet: path: /healthz
port: 5678 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /healthz
port: 5678 initialDelaySeconds: 15 periodSeconds: 5 volumes: - name: n8n-data
persistentVolumeClaim: claimName: n8n-data-pvc
--- apiVersion: v1
kind: PersistentVolumeClaim
metadata: name: n8n-data-pvc
namespace: n8n
spec: accessModes: - ReadWriteOnce
resources: requests: storage: 2Gikubectl apply -f n8n-deployment.yamlCek status:
kubectl get pods -n n8n
kubectl logs -f deployment/n8n -n n8nStep 5 — Expose dengan Ingress
Kalau cluster kamu punya Ingress Controller (nginx-ingress atau Traefik), setup Ingress-nya:
# ingress.yaml apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: name: n8n-ingress
namespace: n8n
annotations: nginx.ingress.kubernetes.io/proxy-body-size: "50m" nginx.ingress.kubernetes.io/proxy-read-timeout: "300" nginx.ingress.kubernetes.io/proxy-send-timeout: "300" # Kalau pakai cert-manager untuk TLS otomatis: cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: nginx
tls: - hosts: - n8n.domain.com
secretName: n8n-tls
rules: - host: n8n.domain.com
http: paths: - path: /
pathType: Prefix
backend: service: name: n8n
port: number: 80Ganti n8n.domain.com dengan domain kamu, dan pastikan DNS sudah diarahkan ke IP Ingress Controller.
kubectl apply -f ingress.yamlCek apakah Ingress sudah dapat address:
kubectl get ingress -n n8nStep 6 — Verifikasi
Kalau semua sudah jalan, buka browser ke https://n8n.domain.com. Kamu akan disambut halaman login n8n.
Cek semua pod berjalan normal:
kubectl get all -n n8nOutput yang diharapkan:
NAME READY STATUS RESTARTS AGE
pod/n8n-xxx-yyy 1/1 Running 0 5m
pod/postgres-0 1/1 Running 0 8m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
service/n8n ClusterIP 10.96.x.x <none> 80/TCP
service/postgres ClusterIP None <none> 5432/TCP
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/n8n 1/1 1 1Troubleshooting Umum
Pod n8n CrashLoopBackOff
Hampir selalu masalah koneksi ke database. Cek log:
kubectl logs deployment/n8n -n n8n --previousPeriksa apakah hostname database benar. Dari dalam cluster, format-nya postgres.n8n.svc.cluster.local — bukan localhost, bukan postgres saja.
Webhook tidak menerima request dari luar
Pastikan env WEBHOOK_URL diisi dengan URL publik yang benar, termasuk trailing slash. n8n pakai ini untuk generate webhook URL yang dikasih ke service lain.
PVC stuck di Pending
Di lokal tanpa dynamic provisioner, kamu perlu buat PersistentVolume manual, atau install local-path-provisioner:
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'Timeout saat akses lewat Ingress
Cek annotation proxy-read-timeout di Ingress. Beberapa workflow n8n yang berat bisa timeout kalau nilai default-nya terlalu kecil.
Scaling ke Multiple Replicas
Begitu mau scale n8n ke lebih dari satu replica, ada hal yang perlu diperhatikan.
Pertama, PVC harus pakai ReadWriteMany karena beberapa pod perlu akses storage yang sama. Tidak semua storage class support ini — cek dulu provisioner kamu.
Kedua, session harus stateless atau disimpan di database. Secara default n8n sudah handle ini dengan baik.
Ketiga, untuk webhook, perlu konfigurasi ekstra agar request masuk ke pod yang tepat atau semua pod bisa handle webhook yang sama.
Cara scale:
kubectl scale deployment n8n --replicas=3 -n n8nTapi sekali lagi, pastikan storage class-nya support ReadWriteMany dulu. Kalau tidak, pod kedua dan ketiga akan gagal mount volume dan langsung stuck.
Cara Pakai Helm (Alternatif Lebih Cepat)
Kalau tidak mau manage YAML satu per satu, ada community Helm chart untuk n8n:
helm repo add open-8gears https://storage.googleapis.com/open-8gears.appspot.com
helm repo update
helm install n8n open-8gears/n8n \ --namespace n8n \ --create-namespace \ --set db.type=postgresdb \ --set externalPostgresql.host=postgres.n8n.svc.cluster.local \ --set externalPostgresql.password=password-kamuSaya pribadi lebih suka cara manual dulu minimal sekali — supaya tahu persis apa yang jalan dan kenapa. Helm bagus untuk deployment kedua dan seterusnya.
Upgrade n8n
Ganti versi image di deployment, lalu apply:
image: n8nio/n8n:1.x.x # ganti dengan versi terbarukubectl set image deployment/n8n n8n=n8nio/n8n:1.x.x -n n8n
kubectl rollout status deployment/n8n -n n8nKalau ada masalah, rollback:
kubectl rollout undo deployment/n8n -n n8nPenutup
Setup di atas cukup untuk production kecil-menengah. Yang perlu diperkuat kalau traffic mulai besar: backup PostgreSQL secara otomatis, monitoring dengan Prometheus/Grafana, dan network policy yang lebih ketat.
Tapi untuk mulai? Ini sudah lebih dari cukup.
Kalau nyangkut di bagian tertentu, cek dulu kubectl describe dan kubectl logs — sembilan dari sepuluh masalah Kubernetes terjawab dari sana.