If you are an SMB owner or the ops generalist who wears ten hats, this guide is your shortcut to a production-ready n8n stack. Copy the .env and docker-compose.yml below, bring it up in minutes, and retain your workflow data across restarts and upgrades. We also cover security, backups, upgrades, and a fast Railway.app path for those who want zero server management.
Our goal is a stable, low-friction setup you can actually maintain. No guesswork on environment variables. No lost workflows during an update. Just a reliable n8n Docker + Postgres stack you can trust in production.
TL;DR - Setup in 5 Steps
Copy the .env template below into your project folder. Paste the docker-compose.yml with n8n and Postgres, complete with named volumes and healthchecks. Set N8N_ENCRYPTION_KEY and enable basic auth before your first run. Start with docker compose up -d and test at your host on port 5678. Then put n8n behind a reverse proxy with TLS and schedule backups.
Want the quickest path without servers? See the Railway.app section below.
Why self-host n8n with Docker and Postgres
For SMBs, Docker gives you clean, repeatable deployments and painless upgrades. Postgres gives durability and performance as your automation workloads grow. Together they form a restart-safe backbone so you never lose workflows, credentials, or execution history when a container restarts.
Persistence comes from named volumes for both n8n and Postgres. Upgrades are just pulling a new image and recreating the container. You get portability across dev, staging, and production without rewriting configs, plus healthchecks and restart policies that keep things running without constant babysitting.
n8n Docker Postgres Environment Variables (Full Reference)
This is the complete set of environment variables you need. Copy this into a file named .env inside your project folder.
# ---------- Postgres ----------
POSTGRES_USER=n8n
POSTGRES_PASSWORD=supersecret_change_me
POSTGRES_DB=n8n
# ---------- DB_TYPE and Postgres connection for n8n ----------
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
DB_POSTGRESDB_USER=${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
DB_POSTGRESDB_SCHEMA=public
# If your DB requires SSL, set:
# DB_POSTGRESDB_SSL=true
# DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED=false
# ---------- n8n instance ----------
# If running locally without HTTPS:
N8N_HOST=localhost
N8N_PORT=5678
N8N_PROTOCOL=http
# If you will expose n8n on a domain with TLS, set:
# N8N_HOST=automation.example.com
# N8N_PROTOCOL=https
# External URL for webhooks. Must match your public host and protocol.
WEBHOOK_URL=http://localhost:5678
# Timezone for executions and cron nodes
GENERIC_TIMEZONE=UTC
TZ=UTC
# Disable telemetry if you prefer
N8N_DIAGNOSTICS_ENABLED=false
# ---------- Security ----------
# Set before first run. Use a long random string. Keep it secret.
N8N_ENCRYPTION_KEY=replace_with_a_long_random_secret
# Enable built-in basic auth for the editor UI
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=change_me_now
# Uncomment only if hosting n8n behind a subpath, for example /n8n
# N8N_PATH=/n8n
# N8N_EDITOR_BASE_URL=https://automation.example.com/n8n/
# Optional: limits for executions and logs
# EXECUTIONS_DATA_SAVE_ON_ERROR=all
# EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
DB_TYPE=postgresdb and what each variable does
DB_TYPE must be set to postgresdb for n8n to use Postgres instead of the default SQLite. This is the single variable that switches the database engine, and getting it wrong is the most common reason for "connection refused" errors.
DB_POSTGRESDB_HOST should match the Docker Compose service name - in our case, postgres. Do not use localhost here because n8n runs in a separate container and needs to reach Postgres over the Docker network.
DB_POSTGRESDB_DATABASE, DB_POSTGRESDB_USER, and DB_POSTGRESDB_PASSWORD must match the POSTGRES_* variables that configure the Postgres container itself. If there is a mismatch, n8n will fail to authenticate on startup.
N8N_ENCRYPTION_KEY encrypts credentials at rest inside Postgres. Set this before your first run and never lose it. If you lose this key, all saved credentials become unrecoverable.
N8N_BASIC_AUTH_ACTIVE, N8N_BASIC_AUTH_USER, and N8N_BASIC_AUTH_PASSWORD protect the editor UI. Always enable this, especially if the instance is reachable from the internet.
docker-compose.yml for n8n and Postgres (copy-paste, production-ready)
Create this file next to your .env. It includes healthchecks, named volumes, and a depends_on condition so n8n waits for Postgres to be ready before starting.
version: "3.8"
services:
postgres:
image: postgres:16-alpine
container_name: n8n-postgres
restart: unless-stopped
env_file:
- .env
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
n8n:
image: n8nio/n8n:latest
# For production, pin to a specific version tag to control upgrades
# image: n8nio/n8n:1.74.0
container_name: n8n-app
restart: unless-stopped
env_file:
- .env
environment:
- DB_TYPE=${DB_TYPE}
- DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}
- DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}
- DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}
- DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}
- DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}
- DB_POSTGRESDB_SCHEMA=${DB_POSTGRESDB_SCHEMA}
- N8N_HOST=${N8N_HOST}
- N8N_PORT=${N8N_PORT}
- N8N_PROTOCOL=${N8N_PROTOCOL}
- WEBHOOK_URL=${WEBHOOK_URL}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${TZ}
- N8N_DIAGNOSTICS_ENABLED=${N8N_DIAGNOSTICS_ENABLED}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE}
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
ports:
- "5678:5678"
user: "1000:1000"
depends_on:
postgres:
condition: service_healthy
volumes:
- n8n_data:/home/node/.n8n
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"]
interval: 15s
timeout: 5s
retries: 20
volumes:
n8n_data:
postgres_data:
Docker Compose Volumes: /home/node/.n8n Explained
The volume n8n_data:/home/node/.n8n is where n8n stores its local configuration, encryption metadata, and any file-based state. Even though your workflow data and credentials live in Postgres, n8n still writes to /home/node/.n8n for things like the encryption key metadata and internal caches.
If you skip this volume mount, you will lose this state every time the container restarts. That can cause issues where n8n can no longer decrypt credentials stored in Postgres because the local key material is gone.
The postgres_data:/var/lib/postgresql/data volume persists the actual database. Named volumes are managed by Docker and survive container recreation, which is why we use them instead of bind mounts. Fewer permission headaches, more portable across machines.
The compose runs n8n as user: "1000:1000". On Linux, make sure the named volume is readable by UID 1000. With named volumes this usually works out of the box. If you switch to a bind mount, you will need to chown -R 1000:1000 /path/to/your/n8n_data_dir.
Postgres Version Requirements for n8n
n8n supports all actively maintained PostgreSQL versions. As of writing, that means Postgres 13 through 17. Our compose file uses postgres:16-alpine which hits the sweet spot of stability, performance, and long-term support.
Avoid Postgres 12 or older - n8n may work on them but they are past end-of-life and you will not get security patches. If you are starting fresh, go with 16. If you are already on 14 or 15, no need to migrate just for n8n.
The Alpine variant (postgres:16-alpine) keeps the image small. There is no functional difference from the Debian-based image for this use case.
Prerequisites
You need a Linux server or Mac with Docker and Docker Compose installed, a user with Docker permissions, and a domain for HTTPS if you plan to expose it on the internet.
Install Docker and the compose plugin for your OS, then verify:
docker --version
docker compose version
Create a project folder:
mkdir n8n && cd n8n
Put your .env and docker-compose.yml here.
Bring it up and test
Start the stack from your project folder:
docker compose up -d
docker compose ps
Check logs if needed:
docker compose logs -f postgres
docker compose logs -f n8n
Confirm the DB connection works
Open your browser to http://localhost:5678 if running locally. Log in with the basic auth credentials from your .env. Create a test workflow, execute it, then restart the n8n container and confirm everything persists. That verifies both the Postgres connection and volumes are working.
If you plan to expose n8n on the internet, do not leave it on raw HTTP. Put it behind a reverse proxy with TLS and set N8N_PROTOCOL=https and N8N_HOST to your domain. Update WEBHOOK_URL to match.
Basic security checklist
Set N8N_ENCRYPTION_KEY before first run. Use a long random string and store it safely. Enable N8N_BASIC_AUTH_ACTIVE with strong credentials - use a passphrase and a unique username, not "admin/admin". Consider an IP allowlist at your reverse proxy so only your office or VPN can reach the editor.
Reverse proxy + TLS (NGINX, Caddy, or Traefik)
Terminate TLS at a reverse proxy. Caddy auto-provisions certificates. NGINX plus Certbot is a solid classic. Traefik integrates well with Docker labels. Make sure your proxy forwards X-Forwarded-Proto and X-Forwarded-For so n8n knows it is behind HTTPS.
Update your environment variables to match. Set N8N_PROTOCOL=https, N8N_HOST=automation.example.com, and WEBHOOK_URL=https://automation.example.com. If hosting under a subpath, set N8N_PATH and N8N_EDITOR_BASE_URL and configure the proxy to preserve the path.
Firewall and updates
Block external access to Postgres. Only the n8n container should reach it over the Docker network. Allow only ports 80 and 443 on the host if using a reverse proxy - close 5678 to the public internet. Pin container image versions in production and plan upgrades during maintenance windows.
Backups and upgrades
A small weekly routine will save you hours during an outage.
Dump and restore Postgres
# Dump to a timestamped file
docker exec -t n8n-postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > n8n_$(date +%F).sql
# Restore from a dump
cat n8n_2025-09-16.sql | docker exec -i n8n-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"
Back up the n8n_data volume too - it holds encryption metadata:
# Backup
docker run --rm -v n8n_data:/data alpine tar czf - -C / data > n8n_data_$(date +%F).tgz
# Restore
cat n8n_data_2025-09-16.tgz | docker run --rm -i -v n8n_data:/data alpine tar xzf - -C /
Store backups off the server. Test restores periodically.
Upgrade n8n safely
Back up Postgres and n8n_data first. Pin a version in docker-compose.yml for production. Pull the new image and recreate:
docker compose pull n8n
docker compose up -d n8n
Watch logs for migrations and confirm the editor works. If something breaks, stop the container, revert the image tag, and restore from your backup.
Railway.app: the fastest path
If you want zero infrastructure, Railway can host n8n in minutes. Create an account, start from an n8n template, add managed Postgres if the template does not include it, and set your N8N_ENCRYPTION_KEY, basic auth variables, and WEBHOOK_URL. Deploy and your first workflow is live.
This is ideal for pilots and SMBs who want results now without touching servers. If you outgrow it, you can lift and shift to Docker with the same variables.
Troubleshooting common issues
Postgres connection refused
The most common cause is a mismatch between DB_POSTGRESDB_HOST and your compose service name. Make sure DB_POSTGRESDB_HOST=postgres and DB_TYPE=postgresdb. Check that Postgres is healthy with docker compose ps and docker compose logs -f postgres. Verify that user and password match between the POSTGRES_* and DB_POSTGRESDB_* variables.
WEBHOOK_URL mismatch
Set WEBHOOK_URL to the exact public base URL that external services will use to reach n8n. If using HTTPS, set N8N_PROTOCOL=https. Behind a subpath, set N8N_PATH and N8N_EDITOR_BASE_URL consistently. Restart n8n after changes: docker compose up -d n8n.
Volume permission issues
The compose runs n8n as user 1000. With named volumes this usually works fine. If you use a bind mount, fix ownership with sudo chown -R 1000:1000 /path/to/your/n8n_data_dir. Prefer named volumes for portability and fewer permission headaches.
Need help? Contact n8nlogic
If you want a second set of eyes or a done-for-you install, we set up n8n for SMBs every week. From a secure Docker stack to a managed Railway deployment, we make sure it is persistent, backed up, and production-ready. Reach out to n8nlogic and skip the guesswork.
FAQ
What should DB_TYPE be for Postgres in n8n?
Set DB_TYPE=postgresdb in your .env file. This tells n8n to use PostgreSQL instead of the default SQLite. The value is postgresdb, not postgres - this trips people up. You also need the corresponding DB_POSTGRESDB_HOST, DB_POSTGRESDB_DATABASE, DB_POSTGRESDB_USER, and DB_POSTGRESDB_PASSWORD variables.
Which environment variables does n8n need for Postgres?
The required variables are DB_TYPE=postgresdb, DB_POSTGRESDB_HOST (set to your compose service name, usually postgres), DB_POSTGRESDB_PORT (default 5432), DB_POSTGRESDB_DATABASE, DB_POSTGRESDB_USER, DB_POSTGRESDB_PASSWORD, and optionally DB_POSTGRESDB_SCHEMA. On the Postgres side, set POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB to match.
Why does the Docker Compose mount /home/node/.n8n?
The /home/node/.n8n directory stores n8n's local configuration and encryption key metadata. Even with Postgres as your database, n8n writes files here that are needed to decrypt credentials. Without this volume, restarting the container would wipe this state and potentially make your stored credentials unrecoverable.
What Postgres versions work with n8n?
n8n supports all actively maintained PostgreSQL versions - currently Postgres 13 through 17. We recommend Postgres 16 for new installs. Avoid anything below 13 since those versions are past end-of-life and no longer receive security updates.
How is this different from the official n8n-hosting withPostgres compose file?
The official n8n-hosting/docker-compose/withPostgres repository gives you a minimal starting point. This guide adds healthchecks so n8n waits for Postgres to be ready, uses a dedicated .env file for cleaner variable management, covers backup and restore commands, includes security hardening with basic auth and encryption, and walks through reverse proxy and TLS setup for production use.