# infra-n8n A reusable n8n deployment: Postgres, the workload, TLS and an ingress, with every environment value left as a `${VAR}` for the caller to substitute. **Nothing here is specific to a deployment.** No hostname, no issuer, no Vault path, no image tag. A consumer supplies those through its own Flux `Kustomization` and its `postBuild.substitute` block — the same shape used for any templated estate. This repo is the template; it is not itself deployed. ## Using it Create, in the repo that owns the deployment: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: { name: infra-n8n, namespace: } spec: interval: 5m url: ref: { branch: main } --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: { name: n8n, namespace: } spec: interval: 10m sourceRef: { kind: GitRepository, name: infra-n8n } path: ./k8s prune: true wait: true postBuild: substitute: NAMESPACE: # … every variable in the table below ``` The namespace must exist first, and so must the AppRole Secret the store reads. ## Variables | Variable | What it is | |---|---| | `NAMESPACE` | where everything lands | | `N8N_HOSTNAME` | the public host. It goes into `WEBHOOK_URL` and the editor's links, so it must be what callers outside the cluster use — an in-cluster name here produces webhooks nothing can call | | `N8N_IMAGE`, `POSTGRES_IMAGE` | pinned images, tag included | | `N8N_STORAGE`, `POSTGRES_STORAGE` | claim sizes, e.g. `2Gi`, `5Gi` | | `TIMEZONE` | e.g. `Europe/Paris`; drives cron triggers | | `INGRESS_CLASS`, `INGRESS_BODY_SIZE` | e.g. `nginx`, `"50m"` | | `TLS_ISSUER_NAME`, `TLS_ISSUER_KIND` | a cert-manager issuer, e.g. `ClusterIssuer` | | `VAULT_SERVER`, `VAULT_MOUNT`, `VAULT_SECRET_PATH` | where the two secrets live | | `VAULT_STORE_NAME`, `VAULT_APPROLE_SECRET` | the `ClusterSecretStore` this creates, and the AppRole Secret in `external-secrets` it authenticates with | | `EXTRA_ENV_SECRET` | a Secret whose keys become environment variables for workflows. Optional in intent — supply an empty Secret if there is nothing | | `EXTRA_CA_CONFIGMAP` | a ConfigMap with `ca.crt`, mounted and set as `NODE_EXTRA_CA_CERTS`, so workflows can call internal TLS endpoints without verification being disabled. Marked `optional`, so an absent one is not an error | | `BLOCK_ENV_ACCESS` | `"true"` blocks `$env` in workflow expressions. n8n's default, and the right one wherever more than one team writes workflows: any workflow can otherwise read any variable in the pod. `"false"` is how a secret reaches a workflow without being committed in its JSON | | `EXECUTIONS_MAX_AGE_HOURS` | how long executions are kept. n8n keeps them for ever by default, which turns the database into a log | ## Two secrets, both of which must outlive any pod Read from `VAULT_SECRET_PATH` as a whole, so the entry must hold exactly: - `DB_PASSWORD` - `N8N_ENCRYPTION_KEY` — encrypts the credentials n8n stores in its database. **Never regenerate it.** A new key does not re-encrypt what is stored; it orphans it. The workflows survive, every credential in them does not. The AppRole's policy should reach `VAULT_SECRET_PATH` and nothing else, so a workload here cannot read another deployment's secrets. The commands are in `k8s/secrets/clustersecretstore.yaml`. ## Choices worth knowing **One replica, and `strategy: Recreate`.** n8n separates editor from workers only in queue mode, which needs Redis; without it a second replica is two schedulers racing over the same cron triggers. And the data volume is ReadWriteOnce, so a rolling update deadlocks — the new pod waiting for a volume the old one has not released. **Postgres is a StatefulSet with a `volumeClaimTemplate`.** The claim is not deleted with the workload, so losing the database to a `kubectl delete` takes more than one mistake. **The certificate is issued, never committed.** The private key is generated in the cluster and rotated by cert-manager. ## What this repo deliberately does not do **Authentication.** n8n's own user management is left at its defaults. A fresh instance is *unclaimed*: anyone who can reach it can create the owner account. Claim it immediately, and put it behind an identity-aware proxy or n8n's SSO before it holds anything that matters. **Backups.** The Postgres claim survives a delete of the workload; it does not survive the cluster.