Deployment inventoryVersion awareChecked 2026-11-06

n8n Docker Compose baseline: pin the release and inventory every service

Compose is a useful boundary when you can name each image, persistent artifact, exposed route, privilege and update owner. Treat upstream manifests as evidence to inspect, not as a universal production recipe. If the host already contains data or its topology is uncertain, stop before replacing files or volumes.

Inventory before change — services, state and routes
Conditional verdict. Use a custom, versioned Compose inventory only after you can account for its state, image tags, key material, public route and recovery owner. The official manual Compose note of at least 4 GB RAM and 2 vCPUs is specifically for its bundled Docker-in-Docker Assistant sandbox; it is not a bare n8n minimum. If this is an occupied host, an unknown database, or a major update without a reviewed migration report, perform read-only discovery and stop. Do not paste a fresh stack over it.

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.

ReferenceCaptured contents and scopeOperator 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

ServiceSelection and versionNetwork / privilegeState and readiness owner
Reverse proxyChosen implementation and exact image tagOnly approved public 80/443 route; no elevated privilege by defaultCertificate/config storage; operator tests external route
n8n mainRequired; explicit reviewed tag or digestPrivate app network; no direct public 5678 by default.n8n, stable encryption key, database; harmless workflow check
PostgresSupported selected image/tag and matching data layoutPrivate app network; no public 5432Named data volume; database and independent recovery owner
External Code runnerRequired for this reference; same n8n release relationship verifiedPrivate broker path; no public listenerConfiguration and logs; runner job validated separately
Queue Redis/workersRejected unless Article 3 topology selectedPrivate broker networkRedis/worker state and drain owner per queue design
Assistant/search/modelRejected unless explicitly selected; separate deltasPrivate endpoints; sandbox privilege reviewCerts, 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.

  1. Record host OS/architecture, Docker engine and Compose v2 versions, root/container permissions, disk space and the selected release's prerequisites.
  2. Find all existing Compose projects, containers, listeners, volumes, networks, domain/proxy routes, database type/version and backup owner.
  3. Capture only names, image references, mount destinations/sources, port mappings and status. Never capture environment values, secret files or decrypted credentials.
  4. 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.
  5. 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.
  6. Review a blank .env.example; keep actual .env outside VCS and logs with operator-controlled permissions. Generate secrets through the approved key process; see key and credential boundaries.
  7. 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:

  1. Record exact image tags or digests, architecture and the release notes reviewed; verify n8n and external runner matching guidance.
  2. Confirm database image/path support, database connectivity, project directory ownership, persistent mounts and stable encryption-key custody. Restart-decryption test must preserve existing credentials.
  3. Review version-controlled files, rendered config handling, logs and support captures for secret exposure. Verify actual file permissions, not assumptions.
  4. Inspect every host listener, Docker publication, firewall layer and external route; approve only the intended proxy path.
  5. 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.
  6. 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.
  7. For webhooks, test from an external caller against the canonical URL and TLS route using Article 6. Localhost success is insufficient.
  8. 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.

Acceptance status rule: mark each item pass, fail or not checked. Any unknown state artifact, mismatched release, public private-service port, missing stable key, unsupported database path or unproven recovery blocks valuable workloads. Record the exact missing evidence and owner; do not label the stack production-ready on parser success.

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.

GateEvidence to retainStop condition
Identify source and targetExact installed source release, exact target tag, channel/release notes and selected migration reportTag or channel meaning unresolved; report not reviewed
Scope workflow impactReport output, node/expression changes, binary paths, community package inventory and owner decisionsCritical path unowned or replacement not tested
Protect recoveryDatabase and .n8n/key coverage, independent copy location, compatible restore evidenceOnly a volume/snapshot exists or key unavailable
Apply and acceptOne controlled update, harmless workflow and external webhook checks, service observationsUnexpected 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.

BoundaryOwner to nameEvidence / cost hand-off
Host, engine, firewallHost operator or provider for its documented scopeOS access, container privileges, packet filtering, IP and stop/delete terms
Images and app schemaRelease operatorRegistry/tag, patches, migration report and selected release notes
Database and volumesDatabase/recovery operatorImage/path, storage, independent copy and restore evidence
Keys and certificatesSecurity/operator roleStable key custody, proxy cert renewal, sandbox mTLS if selected
Proxy and caller pathNetwork/application operatorDomain, intended listeners, external callback, firewall and TLS
Optional processesNamed queue, Assistant, search or model ownerSelected/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

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.