feat: deploy n8n on the cluster

The runtime for the estate's autonomous agents. Postgres as a StatefulSet with
its own claim, n8n behind a cert-manager certificate and the nginx ingress, and
both secrets in Vault behind an AppRole scoped to n8n/* alone.

The certificate is issued into the cluster rather than copied into the repo, so
the private key never reaches git. The CA certificate is committed and mounted
as NODE_EXTRA_CA_CERTS, so a workflow can reach keycloak and the domain
gateways without anyone disabling verification to make it work.

One replica and strategy: Recreate, both deliberate — n8n needs Redis and queue
mode before a second scheduler is safe, and a ReadWriteOnce volume deadlocks a
rolling update.

N8N_ENCRYPTION_KEY is generated once and kept in Vault. Regenerating it does not
re-encrypt the stored credentials, it orphans them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-29 12:34:40 +02:00
commit dc13738e86
17 changed files with 552 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: n8n
namespace: n8n
spec:
# One replica. n8n's queue mode splits the editor from workers and needs Redis
# to do it; without that, a second replica is two schedulers racing over the
# same cron triggers. See the README before scaling this.
replicas: 1
strategy:
# The volume is ReadWriteOnce, so a rolling update deadlocks: the new pod
# waits for a volume the old pod has not released.
type: Recreate
selector:
matchLabels:
app: n8n
template:
metadata:
labels:
app: n8n
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
initContainers:
- name: wait-for-postgres
image: postgres:16.10-alpine
command:
- sh
- -c
- |
until pg_isready -h n8n-postgres -U n8n -d n8n; do
echo "waiting for postgres..."; sleep 3
done
containers:
- name: n8n
image: docker.n8n.io/n8nio/n8n:1.128.2
ports:
- name: http
containerPort: 5678
env:
- name: DB_TYPE
value: postgresdb
- name: DB_POSTGRESDB_HOST
value: n8n-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: DB_PASSWORD
# Encrypts stored credentials. Held in Vault, never regenerated —
# a new key does not re-encrypt, it orphans everything already
# stored.
- name: N8N_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: n8n-secrets
key: N8N_ENCRYPTION_KEY
# The lab CA, so a workflow can call keycloak.sttlab.local and the
# domain gateways without disabling verification. n8n is Node, hence
# this variable rather than a trust store.
- name: NODE_EXTRA_CA_CERTS
value: /etc/sttlab-ca/ca.crt
envFrom:
- configMapRef:
name: n8n-config
volumeMounts:
- name: data
mountPath: /home/node/.n8n
- name: sttlab-ca
mountPath: /etc/sttlab-ca
readOnly: true
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 60
periodSeconds: 20
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumes:
- name: data
persistentVolumeClaim:
claimName: n8n-data
- name: sttlab-ca
configMap:
name: sttlab-ca