Choose the path before installing anything
| Path | What it is | Choose when |
|---|---|---|
| Path A — Official n8n Self-Hosted Cloud | Managed Builds; preinstall / Postgres / Redis marketing; prepaid renew cliff | Want host-side OS/panel duties |
| Path B — Unmanaged DIY Compose | Root; you pin versions, private DB, proxy, encryption key, backups | Need custom networking or refuse managed image |
Do not run Path B Compose against a Path A managed image. Do not expect Path A support to debug your custom Compose. Checkout SKU identity between the n8n landing and Managed Cloud Builds remains a verify-on-order branch.
Path A — Managed n8n acceptance and day-two
- Complete day-zero acceptance for the managed/n8n order.
- Confirm the editor URL is only on intended HTTPS; set admin authentication immediately.
- Record versions you can see (n8n / database / redis) from panel or SSH if offered—marketing “latest” is not a pin.
- Export workflows and credentials on a cadence you can prove; do not treat vendor offsite backup alone as application-consistent recovery (billing guide).
- Before major vendor updates: export → verify you can re-import on a non-production target when available.
“Unlimited workflow executions” is marketing language, not a capacity lab result. Watch CPU, RAM, disk, and queue depth; scale the Build when measurement—not hope—requires it.
Path B — Unmanaged reference Compose
Use a fresh supported Ubuntu/Debian Unmanaged host from setup. The following is an AiXVPS reference layout, not an official ScalaHosting script. Pin versions from current n8n documentation at the time you deploy.
sudo mkdir -p /opt/aixvps/n8n
sudo chown "$USER":"$USER" /opt/aixvps/n8n
cd /opt/aixvps/n8n
umask 077
# Create .env offline; never commit it. Example keys only:
# POSTGRES_PASSWORD=...
# N8N_ENCRYPTION_KEY=... # generate once; preserve forever
# N8N_HOST=n8n.example.com
# WEBHOOK_URL=https://n8n.example.com/
# docker-compose.yml — illustrative structure; pin image digests/tags yourself
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- pgdata:/var/lib/postgresql/data
networks: [internal]
n8n:
image: n8nio/n8n:1.XX.X # replace with a current pinned tag
restart: unless-stopped
depends_on: [postgres]
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: ${N8N_HOST}
WEBHOOK_URL: ${WEBHOOK_URL}
N8N_PROTOCOL: https
volumes:
- n8n_data:/home/node/.n8n
networks: [internal]
# Publish only via reverse proxy on the host — avoid public 5678.
networks:
internal:
volumes:
pgdata:
n8n_data:
docker compose config --quiet
docker compose up -d
docker compose ps
# Place Caddy/Nginx TLS on 80/443 to the n8n service on the internal network.
# Postgres stays internal-only.
Generate the encryption key once; store it in a password manager. Losing it can lock encrypted credentials. Bound workers, concurrency, and execution pruning from observation—not from a universal optimum. Application-consistent proof = Postgres dump + n8n data volume + key material, restored on a disposable target before you trust it.
Day-two operations common to both paths
| Symptom | Check first |
|---|---|
| DB / app unhealthy | Disk, mounts, credentials, container/service logs |
| 502 / TLS errors | Proxy upstream, DNS, certificate host match |
| Webhook failures | Public URL, firewall, auth, HTTP vs HTTPS settings |
| OOM / disk full | Execution history, binary data, prune settings |
| Unexpected exposure | ss/listeners, proxy config, panel “open port” habits |
Migrating Path A ↔ Path B is a project with exports first—not a one-click conversion.
Stop conditions and handoff
Persistent resource pressure → larger Build/Config or split database; remember managed renew cliffs. Need local GPU models beside n8n → AI when-not-to. Leaving the provider → export then Request Cancellation.