Conditional verdict — Compose is an inventory, not a sizing answer
Choose a topology before choosing YAML. The bounded reference below is plain n8n with supported PostgreSQL, a same-release external Code task runner and one reverse proxy. It excludes Redis, workers, Assistant, search and a resident model. A queue design is a separate decision with its own broker and worker requirements; see queue and concurrency. Assistant's sandbox is an optional service set with a distinct trust boundary. Do not add either delta because an upstream sample happens to contain it.
The official manual Compose guide's 4 GB / 2 vCPU note accompanies its Docker-in-Docker sandbox runner. That privileged service is effectively host-root for threat modeling. Do not reuse that figure as a generic n8n floor or infer a capacity promise from it. A plain instance's needs depend on its own execution shape and must be observed; the cost worksheet prices the selected services once their actual host and storage needs are known.
Release selection also needs a gate. The captured 3.0 release notes and breaking-change documentation do not align cleanly in their release/channel wording: one is a release record while migration text uses early-access phrasing and older channel labels remain in surrounding docs. This is a source conflict, not a reason to guess a minor version. Select an exact published tag, read its matching release notes and migration report, and recheck the pre-publication docs. Do not use blind latest, invent a version, or assume a channel label guarantees compatibility. n8n's 3.0 docs describe Docker as the distribution route for new installs; npm-era installations require a migration decision, not an automatic Compose overwrite.
Fresh branch: proceed only after confirming a dedicated empty target, no existing project, data, listeners or domain mapping to preserve, and an approved recovery plan. Occupied or uncertain branch: inventory read-only, identify state and owners, then make a separate migration plan. Unknown means stop.
Compare official references before composing your own
This table distinguishes the evidence each reference supplies from the choices its reader still owns. It prevents a convenient example from being mistaken for a complete operating contract.
| Reference | Captured contents and scope | Operator decision still required |
|---|---|---|
| Manual n8n Compose [N03] | n8n, optional Assistant sandbox services and search; sandbox runner uses privileged Docker-in-Docker. Its 4 GB / 2 vCPU note is scoped to that stack. | Whether to omit Assistant; exact tags; external exposure; persistent recovery; privilege acceptance; certificate lifecycle and production suitability. |
| n8n-hosting withPostgres [N32, N52, N54] | Postgres image, n8n and matching external runner example; captured Postgres 18 uses image-specific PGDATA=/var/lib/postgresql/data. Init script creates a user/schema arrangement. | Exact release, secret/key provision, path compatibility, network exposure, migration procedure and verified recovery. Example is not a turnkey guarantee. |
| One-line setup [N04] | Fresh-install workflow that writes Compose and .env, with version and upgrade options. | It is not proof of arbitrary existing-image discovery or an in-place major migration. Packaging ownership belongs in the image responsibility decision. |
The examples have different goals. The manual manifest's optional sandbox and published n8n port should be reviewed rather than copied unchanged. The Postgres sample does not establish a default n8n release or supply a default encryption key. Its PGDATA value is tied to that image's data layout; it is not a universal Postgres path and does not convert an existing major-version data directory. PostgreSQL major upgrades need their own supported procedure and a verified backup, as documented in the PostgreSQL 18 upgrade guide.
There is a material mismatch inside the Assistant reference: N03 prose says sandbox services do not use env_file, while captured N53 manifest includes it and supplies selected variables. A prose checklist does not enforce a manifest. Inspect rendered configuration and minimize values available to each container. The same capture raises a world-readable certificate artifact concern; inspect each certificate and key's exact mode, owner and distribution before deciding exposure. Permission scope remains open; do not claim every certificate is public or every file is safe.
One rejected guessed .env.example path was not found and is not evidence. A captured manifest only proves what that file contained at capture time. It does not establish security, restore, uptime, production readiness or compatibility with your selected release.
Record services, persistence, ports and privileges
Fill this matrix from the chosen release and the actual host. “Private” means no published host port; it does not replace network policy. Add a billable once per selected service or resource in the cost walkthrough.
Services and version relationships
| Service | Selection and version | Network / privilege | State and readiness owner |
|---|---|---|---|
| Reverse proxy | Chosen implementation and exact image tag | Only approved public 80/443 route; no elevated privilege by default | Certificate/config storage; operator tests external route |
| n8n main | Required; explicit reviewed tag or digest | Private app network; no direct public 5678 by default | .n8n, stable encryption key, database; harmless workflow check |
| Postgres | Supported selected image/tag and matching data layout | Private app network; no public 5432 | Named data volume; database and independent recovery owner |
| External Code runner | Required for this reference; same n8n release relationship verified | Private broker path; no public listener | Configuration and logs; runner job validated separately |
| Queue Redis/workers | Rejected unless Article 3 topology selected | Private broker network | Redis/worker state and drain owner per queue design |
| Assistant/search/model | Rejected unless explicitly selected; separate deltas | Private endpoints; sandbox privilege review | Certs, search/provider settings and optional model state separately owned |
Do not infer version matching from a familiar tag name. The official withPostgres sample ties its runner image to N8N_VERSION; check matching against the selected release task-runner docs. Those docs require N8N_RUNNERS_ENABLED=true on supported 1.x releases and deprecate it from 2.0, so follow that version boundary rather than carrying the variable forward. In n8n 3.0, manual executions on workers change older assumptions about runner placement. Queue service topology and execution classes are owned by the queue article, not this plain baseline.
Volumes, paths and independent recovery
Inventory every persistent path: n8n's /home/node/.n8n volume including encryption key material; the selected Postgres data path; any configured binary or external storage; proxy certificates and configuration; and optional sandbox artifacts if selected. Verify actual mount sources with the running project rather than relying on file names. The chosen Postgres image controls its data path: the captured Postgres 18 sample deliberately sets PGDATA=/var/lib/postgresql/data, but another image or version may differ.
A named Docker volume persists when a container is replaced; it may also persist after the project is removed. Neither fact makes it an independent, application-consistent backup. A host loss, accidental volume removal, corrupted database or missing encryption key can still make the service unrecoverable. Define copies that cover database, .n8n, key custody, proxy/domain state and any configured external artifacts. Article 4 covers database boundaries; Article 10 owns recovery and migration acceptance. Do not accept “Compose up” or “volume exists” as a restore test.
Ports, network and effective exposure
For the reference, publish only the proxy's intentionally selected 443 route and, if the chosen challenge or redirect requires it, 80. Bind n8n 5678 to loopback only when a host-side diagnostic explicitly needs it; otherwise leave it internal to the project network. PostgreSQL 5432, Redis, runner, Assistant sandbox and model endpoints stay private. The official manifest publishes 5678:5678; that is a visible example setting, not a requirement to expose it publicly.
Docker-published traffic can bypass assumptions based on UFW INPUT/OUTPUT rules. Check actual listeners, Docker packet-filter chains, host firewall, cloud/provider firewall and an external reachability observation. A UFW status line alone cannot prove a published container port is unreachable. Do not publish a privileged sandbox runner. Webhook URLs, proxy headers, TLS and caller paths are covered in the webhook and TLS route; this matrix only records ownership and exposure.
Compose runbook skeleton — discover, render, validate
Run these read-only checks on the intended host as an operator with access to Docker. Substitute a reviewed project name and path; the placeholders below are not a command to target a production directory. Do not print environment files or dump container environment. If Docker access is unavailable, stop and ask the host owner for a redacted inventory.
- Record host OS/architecture, Docker engine and Compose v2 versions, root/container permissions, disk space and the selected release's prerequisites.
- Find all existing Compose projects, containers, listeners, volumes, networks, domain/proxy routes, database type/version and backup owner.
- Capture only names, image references, mount destinations/sources, port mappings and status. Never capture environment values, secret files or decrypted credentials.
- Branch: a verified empty dedicated target may proceed to template review. Any container, volume, listener, existing domain, unknown database or uncertain owner means occupied: stop and plan migration separately.
- Write the release record: exact n8n tag, matching runner tag, supported Postgres image and path, stable key custody, selected proxy, external URL and explicit rejected/selected optional services.
- Review a blank
.env.example; keep actual.envoutside VCS and logs with operator-controlled permissions. Generate secrets through the approved key process; see key and credential boundaries. - Before apply, validate schema/rendered settings without exposing secrets, inspect effective ports and volumes, confirm image architecture/pull space, ownership and independent recovery artifacts. Stop if any variable or health check is unsupported for the selected image.
# Read-only discovery template. Replace only the project selector after review.
docker version --format '{{.Server.Os}}/{{.Server.Arch}} {{.Server.Version}}'
docker compose version
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
docker compose ls --format json
docker volume ls --format 'table {{.Name}}\t{{.Driver}}'
docker network ls --format 'table {{.Name}}\t{{.Driver}}'
ss -lnt
df -h
ufw status
# For an identified container only: metadata, never environment values.
docker inspect --format '{{.Name}} {{json .Mounts}} {{json .HostConfig.PortBindings}}' REPLACE_WITH_REVIEWED_CONTAINER
# For a reviewed project directory, render only after secret-safe tooling is confirmed.
# Do not copy the rendered output into a ticket or log: Compose output may contain secrets.
docker compose --project-directory REPLACE_WITH_REVIEWED_PATH config --quiet
Validation note: config --quiet checks Compose parsing without displaying the resolved configuration. It does not prove the images start, health checks work, database is supported, routes are safe, secrets are hidden from process inspection, or restore succeeds. Do not print or save rendered configuration because it can contain resolved secrets. Use static YAML parsing and validate configuration only with a secret-suppressing method confirmed for the selected Compose version. Do not run the final line against an unknown project. Verify the selected Compose implementation supports the flags before use.
The following bounded reference is a syntax-checked template, not a deploy-ready production file. Its names and values are explicit placeholders. Confirm each environment variable against the chosen n8n release; configure the external runner from its matching official guidance. The proxy fragment is illustrative Caddy routing, not a claim that Caddy is required or that DNS/TLS already work. Keep real secrets outside this page, VCS and terminal history.
# compose.yml — reviewed plain n8n + PostgreSQL + external Code runner + proxy
services:
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION:?set reviewed release}
restart: unless-stopped
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: "5432"
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB:?set database name}
DB_POSTGRESDB_USER: ${POSTGRES_USER:?set database user}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD:?set database password}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:?set stable key}
# Only required for supported 1.x releases; deprecated from 2.0. Confirm selected-version docs.
N8N_RUNNERS_MODE: external
N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
N8N_PROTOCOL: https
N8N_HOST: ${N8N_HOST:?set canonical hostname}
N8N_EDITOR_BASE_URL: ${N8N_EDITOR_BASE_URL:?set canonical editor URL}
N8N_WEBHOOK_URL: ${N8N_WEBHOOK_URL:?set canonical webhook URL}
N8N_PROXY_HOPS: ${N8N_PROXY_HOPS:?set reviewed proxy hop count}
N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:?set runner token}
volumes:
- n8n_data:/home/node/.n8n
networks: [app]
depends_on:
postgres:
condition: service_healthy
runner:
condition: service_started
postgres:
image: postgres:${POSTGRES_IMAGE_TAG:?set supported image tag}
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:?set database name}
POSTGRES_USER: ${POSTGRES_USER:?set database user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set database password}
PGDATA: ${POSTGRES_PGDATA:?confirm for selected image}
volumes:
- postgres_data:${POSTGRES_DATA_PATH:?confirm for selected image}
networks: [app]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
runner:
image: n8nio/runners:${N8N_VERSION:?match reviewed n8n release}
restart: unless-stopped
environment:
N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679
N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:?set runner token}
networks: [app]
proxy:
image: caddy:${CADDY_IMAGE_TAG:?set reviewed proxy tag}
restart: unless-stopped
ports:
- "${PUBLIC_BIND_ADDRESS:?set approved bind}:80:80"
- "${PUBLIC_BIND_ADDRESS:?set approved bind}:443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks: [app]
volumes:
n8n_data:
postgres_data:
caddy_data:
caddy_config:
networks:
app:
internal: false
Keep the input record explicit. Copy this blank example into version control; create a separate protected .env locally and fill it only with values approved for the selected release and host. The example is not a secret store. Do not commit, paste into tickets, print, or capture the real .env, resolved Compose output, or container environment.
# .env.example — placeholders and blank private values; safe to track
N8N_VERSION=REPLACE_WITH_REVIEWED_N8N_VERSION
POSTGRES_IMAGE_TAG=REPLACE_WITH_REVIEWED_POSTGRES_TAG
POSTGRES_DATA_PATH=REPLACE_WITH_IMAGE_DOCUMENTED_DATA_PATH
POSTGRES_PGDATA=REPLACE_WITH_IMAGE_DOCUMENTED_PGDATA
N8N_PROTOCOL=https
N8N_HOST=n8n.example.com
N8N_EDITOR_BASE_URL=https://n8n.example.com/
N8N_WEBHOOK_URL=https://n8n.example.com/
N8N_PROXY_HOPS=1
CADDY_IMAGE_TAG=REPLACE_WITH_REVIEWED_PROXY_TAG
PUBLIC_BIND_ADDRESS=REPLACE_WITH_APPROVED_HOST_BIND
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
N8N_ENCRYPTION_KEY=
N8N_RUNNERS_AUTH_TOKEN=
Use the canonical external origin https://n8n.example.com/ as the review placeholder, then replace the hostname only after domain ownership and routing are confirmed. Here N8N_PROXY_HOPS=1 describes exactly one proxy (Reference A) on the request path, with that last proxy forwarding X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto. It is not a default for a CDN, load balancer, or multiple proxy chain: count and trust the actual hops, then choose and document the value for that path.
# Caddyfile — replace hostname after DNS/address ownership is verified
automation.example.com {
reverse_proxy n8n:5678
}
Template limits to resolve before any use: image names/tags and runner broker variables must match selected official release docs; Postgres tag, PGDATA and mount path must match the chosen image; health command availability must be checked. Do not assume depends_on means application readiness. The Postgres image's initialization variables apply to an empty database directory and do not rewrite credentials in existing data. This template intentionally has no env_file, shell secrets, public database port, queue services, Assistant, or claimed UID/GID. The project network remains outbound-capable for proxy certificate acquisition; that does not publish service ports on the host. Only proxy ports are published. Confirm firewall and provider network behavior. Postgres health and container start do not establish n8n application or workflow readiness; check a harmless workflow after startup. The specific TLS/DNS acceptance belongs to Article 6.
For a fresh deployment, create files only in the approved empty project path after review; then perform a controlled change window. For an occupied deployment, never run down, recreate, remove volumes, or reuse its domain/database as an exploratory step. Capture a redacted inventory, identify every state artifact and key owner, and use Article 10's migration procedure. Rollback is gated on a tested compatible recovery copy and a documented source/target database and application version; if that gate is absent, stop before changing persistent data.
Keep queue and Assistant as explicit deltas
Queue/runner delta
Add Redis, workers or webhook processors only if the queue architecture is selected in Article 3. The queue topology has shared supported database and instance-key boundaries, private broker connectivity, matching versions and runner placement requirements. Re-evaluate ordinary binary filesystem guidance for that selected release. A narrower response-offload exception documented for a queued relay is not a universal RAM allowance or a general filesystem recommendation. Do not add a shared volume as a default cure. Article 3 owns worker operations and drain gates; this page records the extra services, exposures, artifacts and billables.
Assistant/search/sandbox delta
Assistant is a feature that can call a model provider; it is not a resident local model, and neither is the external Code task runner. Search may be another service. Select each explicitly and record provider, network, costs and owner separately. The bundled sandbox's privileged Docker-in-Docker runner is root-equivalent for host risk and must never be publicly exposed. Its 4 GB / 2 vCPU note applies only to that bundled stack.
Generated mTLS certificates in the captured manual stack do not auto-renew. Name an owner for renewal, locations, secure distribution and restart acceptance; send key lifecycle questions to Article 9. Keep only required values available to each service. N03 prose versus N53 manifest differs on env_file; inspect rendered behavior for the pinned revision. Inspect exact certificate and key permissions individually: the capture's world-readable warning is a reason to investigate, not evidence that every artifact has the same permissions. This remains unresolved at source scope.
Any search service, sandbox, model provider or third-party API is a separate cost input. Article 2 owns arithmetic; do not silently treat an unpriced optional component as free.
Acceptance receipt before valuable workflows
Record a dated receipt with operator, host/project, selected tags and evidence location. Check the following on a harmless bounded workflow before moving valuable schedules or public callers:
- Record exact image tags or digests, architecture and the release notes reviewed; verify n8n and external runner matching guidance.
- Confirm database image/path support, database connectivity, project directory ownership, persistent mounts and stable encryption-key custody. Restart-decryption test must preserve existing credentials.
- Review version-controlled files, rendered config handling, logs and support captures for secret exposure. Verify actual file permissions, not assumptions.
- Inspect every host listener, Docker publication, firewall layer and external route; approve only the intended proxy path.
- Check service status, then run a harmless workflow that exercises the Code node and the chosen database path. A green container is not an end-to-end result.
- Replace/recreate a disposable test container and verify expected state persists. Confirm independent backup coverage and a restore result under Article 4/10 before production data.
- For webhooks, test from an external caller against the canonical URL and TLS route using Article 6. Localhost success is insufficient.
- Capture bounded CPU, memory and disk observations before/after the harmless run to inform review; do not turn them into capacity claims or benchmarks.
Compose's depends_on.condition: service_healthy can express a dependency on a configured health check. It does not establish n8n application readiness, zero downtime, failover, successful credentials decryption or a workflow result. Verify that the selected image actually contains the health command. For a reported symptom, preserve timestamps and redacted service logs, inspect the failing dependency and stop at the first unverified boundary; ask the relevant database, proxy or runner owner with the selected tag and sanitized evidence. Do not publish secrets in a support bundle.
Updates and the n8n 3.0 migration gate
n8n's update guidance recommends keeping instances updated and testing changes in a staging environment; captured guidance mentions a monthly cadence. Use the procedure for the installation method and release actually selected, not a generic script. Before every change, record current app and database versions, image tags, compose revision, key custody, volumes, external dependencies and a recovery owner. The page on migration and disaster recovery owns compatible restore and cutover details.
For n8n 3.0, do not treat an image-tag edit as the migration. The captured breaking-change notes describe Docker distribution, changed or removed nodes and expressions, binary storage path transition from binaryData to storage, removal of in-memory as the default binary mode, and changed behavior for Code runner/manual worker executions. The selected migration report inventories affected environment and workflow items, and may create a saved replacement version for supported nodes; publication still controls which version runs. It is not a backup, compatibility guarantee for every community package, or performance proof. Unverified community packages are disabled by default in the described transition; inspect your actual workflows and package dependencies.
| Gate | Evidence to retain | Stop condition |
|---|---|---|
| Identify source and target | Exact installed source release, exact target tag, channel/release notes and selected migration report | Tag or channel meaning unresolved; report not reviewed |
| Scope workflow impact | Report output, node/expression changes, binary paths, community package inventory and owner decisions | Critical path unowned or replacement not tested |
| Protect recovery | Database and .n8n/key coverage, independent copy location, compatible restore evidence | Only a volume/snapshot exists or key unavailable |
| Apply and accept | One controlled update, harmless workflow and external webhook checks, service observations | Unexpected migration or failed acceptance |
Hold or stop new writers as the chosen migration plan requires. Read the exact release notes and report, establish coverage from Article 4, and verify a compatible recovery route before changing persistent data. Update one bounded stage, run the receipt above, then proceed only on observed acceptance. Do not assume an older runtime can read a database already migrated forward. If checks fail, pause cutover and invoke the documented Article 10 recovery gate; rollback is possible only when the source application/database combination and recovery artifacts are known to be compatible. Otherwise preserve evidence and stop for a recovery decision.
Responsibility, cost and exit hand-off
A Compose file delegates no work by itself. Assign one owner per row before deciding that a topology is manageable.
| Boundary | Owner to name | Evidence / cost hand-off |
|---|---|---|
| Host, engine, firewall | Host operator or provider for its documented scope | OS access, container privileges, packet filtering, IP and stop/delete terms |
| Images and app schema | Release operator | Registry/tag, patches, migration report and selected release notes |
| Database and volumes | Database/recovery operator | Image/path, storage, independent copy and restore evidence |
| Keys and certificates | Security/operator role | Stable key custody, proxy cert renewal, sandbox mTLS if selected |
| Proxy and caller path | Network/application operator | Domain, intended listeners, external callback, firewall and TLS |
| Optional processes | Named queue, Assistant, search or model owner | Selected/rejected/unresolved status; count each actual cost once |
For a provider comparison, ask for the exact OS/container privileges, image architecture and registry access, effective firewall behavior, persistent-volume and snapshot coverage, second-host or restore capacity, support boundary, retained storage and stop/delete billing rules. Confirm provider claims on the relevant provider evidence page; this article supplies no brand or plan verdict. Use the total-cost tool for selected hosts, storage and services. A volume on one host is not an exit plan. Compare backups with snapshots and cancel, stop and delete terms before removing the service.
Packaging ownership is discussed in Article 8; key and certificate lifecycle in Article 9; recovery in Article 10. For symptom triage, report the exact redacted tag, observed failing boundary and last known-good artifact, then ask the responsible operator to verify one layer. Missing acceptance evidence is a stop condition, not evidence of a vendor defect. See Methodology for evidence handling.
Sources and updates
- Checked 2026-11-06: n8n manual Docker Compose and captured get-n8n-compose.yml (N03, N53); service scope, sandbox privilege, persistence, port and Assistant documentation conflict.
- Checked 2026-11-06: n8n-hosting withPostgres example, init-data.sh, and README (N32, N52, N54); example paths and version relationships are capture-specific.
- Checked 2026-11-06: n8n one-line setup (N04), task runners (N15), Docker installation (N48), 3.0 migration report (N50), update guidance (N24), binary data (N13), and security overview.
- Checked 2026-11-06: Docker packet filtering and firewalls (N33), volume lifecycle (N34), and Compose service reference (N35); PostgreSQL major upgrade guidance (N36).
Source captures dated 2026-11-06. Manifest and prose differences are stated above. Template syntax validation is recorded in the local completion manifest; it is not a deployment, security review, restore, performance test or production certification.