Installing n8n
n8n deploys like any other platform component: an Installation
CR in your GitHub org's .platform repo references the n8n Installable
and names the destination cluster. The installation-operator reconciles it into an ArgoCD
Application. Everything below is either a capability the cluster must already have (Assumptions)
or a component you add before n8n (Prerequisites).
Before you begin
Assumptions — platform capabilities that must already be present
These are baseline p6m platform capabilities. n8n depends on them but does not install them. If any are missing, contact your platform team rather than adding them yourself.
| Capability | Why n8n needs it | Managed by |
|---|---|---|
| Crossplane + shared compositions | The XPostgresDatabase composition fulfills n8n's PostgresDatabase claim. | Control plane |
Crossplane providers — provider-sql + provider-kubernetes | Supply the postgresql.sql.crossplane.io and kubernetes.crossplane.io APIs the composition binds to. | Control plane / org-operator |
| External Secrets Operator | Bridges Key Vault entries (DB, Redis, license, SMTP password) into Kubernetes Secrets. | Platform baseline |
installation-operator | Reconciles the Installation CR into an ArgoCD Application. | Control plane |
| SSO ext-authz (oauth2-proxy + Auth0, or Keycloak) | Gates the n8n editor / REST / UI at the gateway. | Platform baseline |
If provider-sql or provider-kubernetes are missing, do not add a crossplane-providers
Installation to your .platform repo. Installing the provider bundle per-cluster fights the
org-operator over field ownership — it flips managed fields back to invalid values and blocks the
providers from reconciling. The provider bundle must be installed and managed through the control
plane. If the SQL/Kubernetes provider APIs are unavailable on your cluster, raise it with the
platform team.
Prerequisites — install these first
These are ordinary Installables. Add them to your org's .platform repo (or confirm they're already
Healthy on the cluster) before applying the n8n Installation. Cluster names follow the
{solution}-{env}-{region} convention — see
Installation → Discovering Cluster Names.
| Installable | Provides | n8n dependency |
|---|---|---|
keda | Event-driven autoscaling. | Worker autoscaling on Redis queue depth. |
istio-gateway-api | The public-oidc (SSO-gated) and public-open (unauthenticated) Gateways. | n8n's HTTPRoutes attach to these listeners. |
cnpg | A CloudNativePG cluster and the cnpg-cluster provider-sql ProviderConfig. | The PostgresDatabase claim binds to this ProviderConfig. |
KEDA and the Gateways are stock installs with no overrides:
apiVersion: p6m.dev/v1alpha1
kind: Installation
metadata:
name: keda
spec:
installableRef:
kind: Installable
name: keda
namespace: installables
destinations:
- clusterRef:
name: acme-corp-dev-eastus # your cluster
apiVersion: p6m.dev/v1alpha1
kind: Installation
metadata:
name: istio-gateway-api
spec:
installableRef:
kind: Installable
name: istio-gateway-api
namespace: installables
destinations:
- clusterRef:
name: acme-corp-dev-eastus
CNPG enables both the shared cluster and the provider-sql ProviderConfig. The cluster block is
templated from cluster info (template variables);
the two flags below are the parts that matter for n8n:
apiVersion: p6m.dev/v1alpha1
kind: Installation
metadata:
name: cnpg
spec:
installableRef:
kind: Installable
name: cnpg
namespace: installables
overrides:
source:
template: |
p6m-cnpg-cluster:
enabled: true # the CloudNativePG cluster
# ... cloud/backup config templated from cluster info ...
providerSql:
enabled: true # the cnpg-cluster ProviderConfig the claim binds to
destinations:
- clusterRef:
name: acme-corp-dev-eastus
Install n8n
With the prerequisites Healthy, add the n8n Installation. This minimal form gives you an
SSO-gated editor on n8n.{{ dns.domainName }}, a platform-provisioned Postgres, in-cluster Redis,
and KEDA worker autoscaling:
apiVersion: p6m.dev/v1alpha1
kind: Installation
metadata:
name: n8n
spec:
installableRef:
kind: Installable
name: n8n
namespace: installables
destinations:
- clusterRef:
name: acme-corp-dev-eastus
overrides:
source:
template: |
n8n:
persistence:
# RWX so rolling updates don't block on a single-writer volume.
# The cluster default is RWO-only managed disk — use a ReadWriteMany class.
storageClassName: azurefile-csi
keda:
enabled: true # worker autoscaling on Redis queue depth
Open a PR against your .platform repo and merge. The {github-org}-installations root
application picks it up; watch the child Application reach Synced / Healthy in ArgoCD.
After installing
1. External Secrets reconcile. If you enabled any Key-Vault-backed feature (license, SMTP, bring-your-own connections), confirm the secrets synced:
kubectl -n n8n get externalsecret
# STATUS should be Ready. A SecretSyncedError almost always means the Key Vault
# entry named in your overrides doesn't exist yet — create it, then it self-heals.
2. Pods come up. Expect a main pod and worker pods (plus the CNPG database pods in
cnpg-system):
kubectl -n n8n get pods
3. First sign-in. Reach the UI at https://n8n.<your-domain>/. The first user to sign in
becomes the instance owner — make sure that's your admin. They invite the rest of the team from
inside n8n. (Without SMTP, invite and password-reset links aren't emailed — the owner copies them
manually. Enable SMTP below to have n8n send them.)
Optional features
All off by default. Enable per install by adding to the overrides.source.template.
SMTP — user-management email
Sends invite and password-reset emails. Requires externalSecrets.enabled: true. Seed the Key
Vault entry first — the password is a hard dependency and n8n won't start without it:
az keyvault secret set --vault-name <your-vault> --name n8n-smtp-password --value "<api-key>"
externalSecrets:
enabled: true
storeConfig:
vaultName: platform-{{ azure.uniqueSuffix }}
enableAccessManagement: true
smtp:
enabled: true
host: smtp.sendgrid.net
port: 587
user: apikey
sender: n8n@example.com # must be a domain-authenticated sender
ssl: false
startTls: true
passwordKeyVaultEntry: n8n-smtp-password
External webhooks
By default n8n's only route is SSO-gated, so external callers (Stripe, GitHub, …) POSTing to a
Webhook node get bounced to SSO. Enabling this adds a second HTTPRoute on the unauthenticated
public-open Gateway, exposing only the webhook/form/mcp trigger paths on a dedicated subdomain:
gatewayApi:
webhook:
enabled: true
hostname: n8n-webhooks.{{ dns.domainName }} # must differ from the editor host
Every Webhook node you expose on this route must set its own node-level authentication (Header, Basic, JWT, or an HMAC-verified signature). Without it, the endpoint is open to the internet.
Worker autoscaling
KEDA is enabled in the base install above. It scales workers on Redis queue depth — defaults are
2–20 workers at roughly 5 waiting jobs per worker. Tune min/max under n8n.keda.worker.
Troubleshooting
n8n CrashLoops with permission denied for schema public
A known gap in the shared Postgres composition: the app role can connect but isn't the owner of the
database, so on PostgreSQL 15+ it can't CREATE in the public schema and first-boot migrations
fail. Until the composition grants schema-level privileges, apply the grant out-of-band once the
PostgresDatabase claim is READY, then restart the n8n pods. Ask your platform team to run the
GRANT ... ON SCHEMA public against the CNPG primary — it requires cluster-superuser access.
An ExternalSecret is stuck SecretSyncedError
The Key Vault entry referenced in your overrides doesn't exist (or the name is misspelled). Create
it with az keyvault secret set …; ESO retries automatically.
Never rotate the encryption key
N8N_ENCRYPTION_KEY decrypts every stored credential. The wrapper stores it in an immutable Secret
and never rotates it by design — rotating it destroys every encrypted credential in the database. A
genuine rotation is a from-scratch migration, not a config change.
main pod stuck Pending after a rapid uninstall + reinstall
The persistent volume claim from the previous install can still be Terminating (finalizer delay),
which silently drops the new claim. Re-run the install to re-reconcile — the volume is recreated on
the second pass. It backs only n8n's local cache; workflow and credential data live in Postgres and
are unaffected.