Xiaobai
Developer · Builder
Building AI engineering systems, developer tools and long-term digital assets at XBSTACK.
About Xiaobai & XBSTACK →
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
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
latestimage 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
| Scenario | Recommended Database | Public Access | Queue Mode Needed? |
|---|---|---|---|
| Local trial, small number of manual workflows | SQLite | Not required | No |
| Long-term personal use, scheduled tasks, webhooks | Postgres | VPS reverse proxy or controlled tunnel | Not yet |
| NAS internal deployment, no public IP | Postgres | Cloudflare Tunnel / controlled reverse proxy | Not yet |
| Multi-user usage, steadily growing execution volume | Postgres | Official domain + HTTPS | Monitor queues and resources before deciding |
| Multiple Workers, horizontal scaling | Postgres | Official domain + HTTPS | Yes, 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
- A fixed domain name, e.g.,
n8n.example.com. - An
N8N_ENCRYPTION_KEYthat remains unchanged across container rebuilds. - Database passwords saved separately, never committed to the Git repository.
- 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:
- Postgres databases.
- n8n data volumes.
- Secure copies of
N8N_ENCRYPTION_KEYand other secrets. - 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:
- Record the current image version and database backup timestamp.
- Export critical workflows or record their IDs.
- Start a test instance using the target version.
- Review Release Notes and Breaking Changes.
- Regression test Webhooks, OAuth, Credentials, Code nodes, and AI nodes.
- Update the production image.
- 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_URLand 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
- n8n AI Workflow Production Portal Page
- n8n Webhook Production Implementation
- n8n Error Handling, Retries, Timeouts, and Cost Monitoring
- n8n Queue Mode, Redis, and Worker in Practice
- n8n Gmail Summaries Written to Google Sheets
- n8n Notion Knowledge Base Agent
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 →AI Engineering Weekly
Production changes, real failures, experiments and new XBSTACK assets.
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.