XBSTACK XBSTACK
Xiaobai

Xiaobai

Developer · Builder

Building AI engineering systems, developer tools and long-term digital assets at XBSTACK.

About Xiaobai & XBSTACK →
Self-hosted n8n Docker Compose, Postgres, VPS, and NAS Production Deployment Guide

Self-Hosted n8n Deployment Guide: Docker Compose, Postgres, VPS, and NAS Production Baseline

How to deploy self-hosted n8n for stability? This article provides a production baseline using Docker Compose + Postgres, covering version pinning, N8N_ENCRYPTION_KEY, WEBHOOK_URL

Published · 2026-05-286 min readXBSTACK
#n8n#workflow-automation#self-hosted#docker#postgres

How to deploy self-hosted n8n for stability? This article provides a production baseline using Docker Compose + Postgres, covering version pinning, N8N_ENCRYPTION_KEY, WEBHOOK_URL

2026-07 Update Notes: This article has been refactored from a “working Docker example” to a production baseline. We removed easily outdated plan pricing and usage quotas, eliminated the latest image and database public port examples, and added version pinning, Editor URL configuration, private networking, recovery drills, and Queue Mode upgrade boundaries.

The Key Point: The Minimal Production Stack for Self-Hosted n8n


  ↓
Caddy / Nginx / Cloudflare Tunnel
  ↓
n8n
  ↓
Postgres

This minimal setup is suitable for individual developers, small teams, and AI workflows with low to medium concurrency. It excludes Redis and Worker components, aiming to first ensure that the domain name, credentials, database, and backups are correctly configured.

When you encounter persistent queuing, task execution competing for the main instance’s resources, or the need for horizontal scaling, upgrade to:


  ↓
n8n Main ── Redis ── n8n Worker × N
      └──────── Postgres ────────┘

Don’t cram Queue Mode, multiple Workers, object storage, and complex monitoring all into a single Compose file right from the start. Deployment complexity should be triggered by actual workload, not by tutorial length.

How to Choose Between VPS, NAS, SQLite, and Postgres

ScenarioRecommended DatabasePublic AccessQueue Mode Needed?
Local trial, small number of manual workflowsSQLiteNot requiredNo
Long-term personal use, scheduled tasks, webhooksPostgresVPS reverse proxy or controlled tunnelNot yet
NAS internal deployment, no public IPPostgresCloudflare Tunnel / controlled reverse proxyNot yet
Multi-user usage, steadily growing execution volumePostgresOfficial domain + HTTPSMonitor queues and resources before deciding
Multiple Workers, horizontal scalingPostgresOfficial domain + HTTPSYes, requires Redis + Queue Mode

The n8n official documentation states that self-hosted instances default to SQLite but also support Postgres. Queue Mode officially recommends Postgres and explicitly advises against combining it with SQLite. Choosing a database isn’t about “SQLite will definitely break”; rather, it depends on your requirements for persistent operation, scalability, and recovery.

Four Things to Prepare Before Deployment

  1. A fixed domain name, e.g., n8n.example.com.
  2. An N8N_ENCRYPTION_KEY that remains unchanged across container rebuilds.
  3. Database passwords saved separately, never committed to the Git repository.
  4. A specific image version or digest, avoiding the use of latest.

Recommended directory structure:

n8n-stack/
├── compose.yml
├── .env
├── .env.example
└── backups/

.gitignore must include at least:

.env
backups/
*.sql

Docker Compose Production Baseline

The configuration below intentionally omits mapping Postgres’s 5432 port. n8n communicates with the database over a private Docker network; externally, only n8n behind the reverse proxy needs to be accessible.

services:
  postgres:
    image: ${POSTGRES_IMAGE:?pin POSTGRES_IMAGE}
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-n8n}
      POSTGRES_USER: ${POSTGRES_USER:-n8n}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-n8n} -d ${POSTGRES_DB:-n8n}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

  n8n:
    image: ${N8N_IMAGE:?pin N8N_IMAGE}
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB:-n8n}
      DB_POSTGRESDB_USER: ${POSTGRES_USER:-n8n}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}

      N8N_HOST: ${N8N_HOST:?set N8N_HOST}
      N8N_PROTOCOL: https
      N8N_PORT: 5678
      N8N_EDITOR_BASE_URL: https://${N8N_HOST}/
      WEBHOOK_URL: https://${N8N_HOST}/
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:?set N8N_ENCRYPTION_KEY}

      EXECUTIONS_DATA_PRUNE: "true"
      EXECUTIONS_DATA_MAX_AGE: ${EXECUTIONS_DATA_MAX_AGE:-168}
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE:-Asia/Shanghai}
      TZ: ${GENERIC_TIMEZONE:-Asia/Shanghai}
    volumes:
      - n8n_data:/home/node/.n8n
    expose:
      - "5678"
    networks:
      - frontend
      - backend

networks:
  frontend:
  backend:
    internal: true

volumes:
  postgres_data:
  n8n_data:

Why Pin Images

Pin the N8N_IMAGE and POSTGRES_IMAGE images from .env to verified versions or digests. When upgrading, update the test environment first, verify that database migrations and critical workflows function correctly, and then proceed to update the production environment.

error: N8N_IMAGE=docker.n8n.io/n8nio/n8n:latest
: N8N_IMAGE= digest

This article avoids hardcoding a specific current version as the “always recommended version,” since version numbers change. On deployment day, verify the target version against the n8n Release Notes and official upgrade documentation.

Three Environment Variables That Determine Deployment Success

N8N_ENCRYPTION_KEY

n8n uses this to encrypt credentials in the database. The official documentation states that a random key is automatically generated on first startup, but you can also provide a custom key. In production, store your custom value in a password manager or a controlled secret store.

It must satisfy:

  • Remain unchanged after container rebuilds.
  • Be migrated along with the server during migration.
  • Be identical across the main instance, Workers, and Webhook Processor in Queue Mode.
  • Perform a full database backup and review the official rotation instructions before rotating the key.

Never write the actual value into Compose files, articles, screenshots, or Git repositories.

WEBHOOK_URL

This determines the base URL for production webhooks as seen by external systems. Services like GitHub, Stripe, and Slack must be able to reach this HTTPS address from the public internet.

After deployment, verify:

 Webhook YesNo
 /webhook-test/ /webhook/ YesNo
YesNo
YesNo Host HTTPS

N8N_EDITOR_BASE_URL

The official documentation defines this as the public URL used to access the editor. It is also used for emails sent by n8n and SAML redirect addresses. Even if webhooks are functioning correctly, a mismatched Editor URL can cause login emails, OAuth flows, or admin links to point to internal network addresses.

Reverse Proxy: Expose Only n8n, Not the Database

Using Caddy as an example:

n8n.example.com {
    reverse_proxy n8n:5678
}

Actual deployment also requires checking:

  • Whether DNS points to the correct entry point.
  • Whether HTTPS certificates are renewing normally.
  • Whether upload size limits and proxy timeouts accommodate long-running tasks.
  • Whether WebSocket, SSE, or streaming responses are being interrupted.
  • Whether the admin entry point requires additional access controls.

Postgres and Redis should only be reachable on a private network. Do not expose 5432 or 6379 directly to the public internet just for “convenient remote connections”; use SSH tunnels, VPNs, or controlled jump hosts when maintenance is required.

NAS Deployment: The focus is on the entry point and permissions, not the Docker interface

The differences between NAS and VPS deployments mainly lie in three areas.

1. No public IPv4

Use Cloudflare Tunnel or another entry point with TLS and access control. Do not expose the NAS management panel, Docker Socket, or database ports simultaneously.

2. Mount directory permissions

First, confirm whether the user inside the container can read and write to the persistent directories. Do not treat chmod 777 as a long-term solution; it is more robust to explicitly set the directory owner, UID/GID, and minimum permissions.

3. Limited resources

AI nodes, large file parsing, Code nodes, and concurrent Webhooks can all consume significant memory. First, establish observability before deciding whether to limit container resources. When frequent OOM restarts occur, reduce concurrency, split large files, or migrate to a more suitable host rather than simply increasing the restart limit.

Backups: Preserve at least four categories of assets

Backing up only n8n_data is insufficient, nor is relying solely on pg_dump.

You need to save:

  1. Postgres databases.
  2. n8n data volumes.
  3. Secure copies of N8N_ENCRYPTION_KEY and other secrets.
  4. Compose files, reverse proxy configurations, and version settings.

Database backup example:

docker compose exec -T postgres \
  pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
  > "backups/n8n-$(date +%F).sql"

Before restoring, stop the n8n instance that writes to the database, then import the SQL dump into an empty database. After restoration is complete, do not immediately declare success. At a minimum, verify:

  • Whether you can log in.
  • Whether existing Credentials can be decrypted.
  • Whether three critical workflows can be executed manually.
  • Whether Webhooks return expected results.
  • Whether scheduled tasks and time zones are correct.

Backups without a recovery drill are merely “potentially existing files.”

Upgrading: Backup First, Check Migrations, Then Regression Test Critical Workflows

Recommended sequence:

  1. Record the current image version and database backup timestamp.
  2. Export critical workflows or record their IDs.
  3. Start a test instance using the target version.
  4. Review Release Notes and Breaking Changes.
  5. Regression test Webhooks, OAuth, Credentials, Code nodes, and AI nodes.
  6. Update the production image.
  7. Keep references to the rollbackable old image and database backups.

Do not assume that automatic database migration means “upgrades are always risk-free.” Automatic migrations handle schema changes; they do not verify third-party nodes, expressions, OAuth flows, or business outputs.

When to Upgrade to Queue Mode

Watch for these signals first:

  • Execution wait times are continuously increasing.
  • The main instance’s CPU or memory is saturated over the long term.
  • A large task slows down the editor and other workflows.
  • You need Workers to run on separate machines.
  • You need rolling scaling or clearer fault isolation.

Key constraints of Queue Mode:

  • EXECUTIONS_MODE=queue.
  • Redis handles queue notifications.
  • Workers execute actual tasks.
  • The main instance and Workers access the same Postgres database.
  • All instances use the same N8N_ENCRYPTION_KEY.
  • The official recommendation is Postgres 13+; SQLite is not recommended for Queue Mode.

For full configuration details, continue reading: n8n Queue Mode, Redis, and Worker in Practice. The basic deployment guide only outlines upgrade boundaries and does not maintain two separate Compose files.

Pre-Launch Acceptance Checklist

[ ] n8n Postgres digest
[ ].env Git
[ ] N8N_ENCRYPTION_KEY security
[ ] Postgres
[ ] WEBHOOK_URL HTTPS
[ ] N8N_EDITOR_BASE_URL HTTPS
[ ] process
[ ] EXECUTIONS_DATA_PRUNE configuration
[ ] databasesuccess
[ ]
[ ] Webhook, OAuth

Common Issues

Webhook Returns 404

First, distinguish between the following:

  • The test URL /webhook-test/ is only valid during the test listening period.
  • The production URL /webhook/ requires the workflow to be enabled.
  • If there is a domain or protocol error, check WEBHOOK_URL and proxy headers.
  • If the route is correct but the upstream is inaccessible, check DNS, TLS, firewalls, and tunnels.

Credentials Cannot Be Decrypted

The most common cause is that N8N_ENCRYPTION_KEY has changed or been lost. Do not delete the Credential record in the database as your first response; instead, recover the original key and verify that all instances are consistent.

Database Connection Failed

Check:

DB_TYPE
DB_POSTGRESDB_HOST
DB_POSTGRESDB_PORT
DB_POSTGRESDB_DATABASE
DB_POSTGRESDB_USER
databaseuserpermission
Docker network
Postgres healthcheck

Worker Connection to Redis Timed Out

Ensure the main instance and the Worker use the same Queue configuration, that the Redis address is reachable within the container network, and that you haven’t mistakenly treated localhost as another container.

Official Documentation

Continue Reading

Topic path / AI workflows

Continue through the production automation path

The workflow hub connects self-hosting, queue mode, webhooks, retries, observability and n8n implementation cases into one production-oriented learning path.

More to Explore

Topic hub →
n8n Webhook Production URL: Test vs Production, WEBHOOK_URL, Reverse Proxy, and AuthFix n8n Production URL issues by publishing the workflow, setting WEBHOOK_URL behind a reverse proxy, forwarding proxy headers, and validating auth and idempotency.n8n Queue Mode + Redis in Practice: When to Offload Workflows to a Queuen8n Queue Mode + Redis in Practice: A hands-on guide to deploying n8n Queue Mode, Redis, and Workers in production.n8n 2.33.7 Distroless ARM64 GLIBC_PRIVATE Error: Reproduction and Workaroundn8n 2.33.7 distroless on ARM64 exits 127 with __tunable_is_initialized / GLIBC_PRIVATE. Compare 2.26.9, the Dockerfile ABI boundary, and a verified rollback workaround.n8n Gmail Summarizer: Extract Action Items to Google Sheets Step by StepBuild an n8n Gmail summarizer with Gmail Trigger, AI structured extraction, priorities and action items, Message ID deduplication, Google Sheets output, and failure handling.

AI Engineering Weekly

Production changes, real failures, experiments and new XBSTACK assets.

Comments & evidence

DISCUSSION

Questions, verification and corrections

Sign in to comment. Every new comment is reviewed before publication; while pending, it is visible only to you and the administrator.

Sign-in required Reviewed before public
Loading the discussion…