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 will also cover basic security, backups, upgrades, and a super fast Railway.app path.
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
- Create a project folder and add the .env template from this post
- Paste the docker-compose.yml with n8n and Postgres, plus named volumes and healthchecks
- Set N8N_ENCRYPTION_KEY and enable basic auth before first run
- Start the stack with docker compose up -d and test at your host on port 5678
- Put n8n behind a reverse proxy with TLS, then schedule backups
Want the quickest possible path without servers to manage? See the Railway.app section below. You can start a template, set a handful of variables, and have n8n running in minutes.
Why self-host n8n with Docker and Postgres
For SMBs, Docker offers a clean, repeatable deployment and easy upgrades. Postgres gives durability and performance for growing automation workloads. Together, they provide a restart-safe backbone so you do not lose workflows, credentials, or execution history every time a container restarts.
Benefits for SMBs: persistence, reliability, minimal DevOps
- Persistence by default with named volumes for n8n and Postgres
- Smooth upgrades by pulling a new image and recreating the container
- Portability across dev, staging, and production without rewriting configs
- Minimal maintenance using healthchecks, restart policies, and a simple backup plan
- Security controls like encryption at rest for credentials and basic auth at the edge
Prerequisites
You will need:
- A Linux server or Mac workstation with Docker and Docker Compose
- A user with permission to run Docker
- A domain for HTTPS if you plan to expose it on the internet
- A text editor and terminal access
Install Docker and Docker Compose
Install Docker and the docker compose plugin for your OS. After installation, verify:
docker --version
docker compose version
Create a project folder
Pick a folder where your n8n stack will live:
mkdir n8n && cd n8n
You will keep your .env and docker-compose.yml here. Docker named volumes will be created on your host and managed by Docker.
.env template for n8n + Postgres (copy/paste)
Copy this into a file named .env inside your project folder. Adjust values as needed.
# ---------- Postgres ----------
POSTGRES_USER=n8n
POSTGRES_PASSWORD=supersecret_change_me
POSTGRES_DB=n8n
# ---------- Database config 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.
# If local only, leave as http://localhost:5678
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
Required env vars (POSTGRES_, DB_, N8N_*)
- POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB configure the database container
- DB_TYPE must be postgresdb for n8n to connect to Postgres
- DB_POSTGRESDB_HOST should be the compose service name postgres
- N8N_HOST, N8N_PROTOCOL, and WEBHOOK_URL must reflect how users and webhooks will reach n8n
- N8N_PORT is where n8n listens inside the container and on the host if you publish it
Security-sensitive vars (N8N_ENCRYPTION_KEY, Basic Auth)
- N8N_ENCRYPTION_KEY encrypts credentials at rest. Set this before first run and never lose it
- N8N_BASIC_AUTH_ACTIVE with N8N_BASIC_AUTH_USER and N8N_BASIC_AUTH_PASSWORD protects the editor UI
- Rotate secrets periodically and store them in a safe vault or password manager
docker-compose.yml for n8n and Postgres (durable, restart-safe)
Create docker-compose.yml next to your .env. This is a copy-paste baseline with comments, healthchecks, and persistent volumes.
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}
# - N8N_PATH=${N8N_PATH}
# - N8N_EDITOR_BASE_URL=${N8N_EDITOR_BASE_URL}
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:
Services and volumes explained
- postgres uses a small Alpine image and persists its data to the named volume postgres_data
- n8n depends on Postgres health. It exposes port 5678 on the host, stores configuration and credentials in n8n_data, and runs as user 1000 to avoid permission surprises
- Named volumes n8n_data and postgres_data live outside the containers, so restarting or recreating containers does not erase your data
- Healthchecks make startup reliable. n8n waits until Postgres is ready before it begins
Bring it up and test
Start the stack:
docker compose up -d
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
Access n8n and confirm DB connection
Open your browser to http://localhost:5678 if running locally. Log in with the basic auth user and password from your .env. Create a test workflow and execute it. Restart the n8n container, then confirm your workflow persists and execution history remains intact. That verifies 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 your external URL exactly.
Basic security checklist
Security is not a checkbox. It is a habit. Start with the basics and you will already be ahead of most internet-exposed services.
Set encryption key and basic auth
- Set N8N_ENCRYPTION_KEY before first run. Use a long random string. Store it safely
- Enable N8N_BASIC_AUTH_ACTIVE with strong credentials. Consider a passphrase and a unique username
- Consider an allowlist at your reverse proxy for the editor UI so only your office IP or VPN can reach it
Reverse proxy + SSL/TLS (NGINX, Caddy, or Traefik)
- Terminate TLS at a reverse proxy. Caddy can auto provision certificates. NGINX plus Certbot is a solid classic. Traefik integrates nicely with Docker labels
- Ensure your proxy forwards headers X-Forwarded-Proto and X-Forwarded-For so n8n knows it is behind HTTPS
- Update environment variables to match your public URL. For example:
- N8N_PROTOCOL=https
- N8N_HOST=automation.example.com
- WEBHOOK_URL=https://automation.example.com
- If you host under a subpath, set N8N_PATH and N8N_EDITOR_BASE_URL, and configure your proxy to preserve the path
Firewall and updates
- Block external access to Postgres. Only the n8n container should reach it
- Allow only ports 80 and 443 on the host if using a reverse proxy. Close 5678 to the public internet
- Apply OS updates regularly. Pin container image versions in production and plan upgrades during a maintenance window
Backups and upgrades
A small weekly routine will save you hours during an outage. Back up your database and your n8n_data volume, then test a restore.
Dump/restore Postgres
Create a dump from the running container:
# Dump to a timestamped file on the host
docker exec -t n8n-postgres pg_dump -U ""$POSTGRES_USER"" ""$POSTGRES_DB"" > n8n_$(date +%F).sql
Restore from a dump:
# Create the database if needed, then restore
cat n8n_2025-09-16.sql | docker exec -i n8n-postgres psql -U ""$POSTGRES_USER"" -d ""$POSTGRES_DB""
Snapshot the n8n_data volume too. It stores encryption metadata and local settings:
# Backup the n8n_data volume to a tarball
docker run --rm -v n8n_data:/data alpine tar czf - -C / data > n8n_data_$(date +%F).tgz
# Restore the volume from a tarball
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 both dump and restore periodically.
Upgrade n8n safely
- Back up Postgres and the n8n_data volume
- Pin a version in docker-compose.yml for predictable results in production
- Pull the new image and recreate:
docker compose pull n8n docker compose up -d n8n
- Watch logs for migrations and confirm the editor UI works
- If something goes wrong, stop the new container, revert the image tag to the previous version, and restore from your backup
Railway.app: the fastest path
If you want a no-infrastructure approach, Railway can host n8n quickly.
- Create a Railway account
- Start from an n8n template
- Add a managed Postgres plugin if the template does not include one
- Set N8N_ENCRYPTION_KEY, N8N_BASIC_AUTH variables, and WEBHOOK_URL
- Deploy and open the URL. Your first workflow will be live in minutes
This is ideal for pilots and SMBs who want results now and a managed Postgres instance without touching servers.
Start from a template and run it
- Click to deploy the n8n template
- Provision Postgres and copy its connection details into the DB_POSTGRESDB_* variables
- Set N8N_HOST and N8N_PROTOCOL to match the Railway domain, then set WEBHOOK_URL to the same
- Save and redeploy. Test persistence by creating a workflow, redeploying, and verifying it remains
If you outgrow the managed path, you can lift and shift to Docker with the same variables.
Troubleshooting common issues
Even with good defaults, small typos can slow you down. Here is how to fix the top three issues we see.
Postgres connection refused
- Ensure DB_POSTGRESDB_HOST=postgres and DB_TYPE=postgresdb
- Confirm Postgres is healthy:
docker compose ps docker compose logs -f postgres
- Make sure user and password match POSTGRES_USER and POSTGRES_PASSWORD
- If you changed service names, update references in the .env and compose files
- Avoid exposing Postgres to the public network. Keep it on the Docker network only
WEBHOOK_URL mismatch
- Set WEBHOOK_URL to the exact public base URL that external services will call
- If using HTTPS, set N8N_PROTOCOL=https and ensure your reverse proxy terminates TLS
- Behind a subpath, set N8N_PATH and N8N_EDITOR_BASE_URL consistently and configure your proxy to preserve the path
- After changes, restart n8n:
docker compose up -d n8n
Volume permission issues
- The compose runs n8n as user 1000. On Linux, ensure your host directory or named volume is readable by UID 1000
- If you bind mount a host directory, fix ownership:
sudo chown -R 1000:1000 /path/to/your/n8n_data_dir
- Prefer named volumes for portability and fewer permission surprises
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 will make sure it is persistent, backed up, and ready for production. Reach out to n8nlogic and get a reliable automation backbone without the guesswork.
Extra notes for power users
- Production image pinning: lock your n8n image to a tested version, then upgrade on your schedule
- Healthchecks: you can extend retries so n8n waits longer for cold Postgres disks
- Execution data retention: tune EXECUTIONS_DATA_SAVE_ON_SUCCESS and related variables for storage control
- Monitoring: wire up container metrics and alerts so you catch failures early
Author
Artur Lusmägi is the founder of n8nlogic, helping SMBs plan, deploy, and maintain reliable n8n automations. He has deployed dozens of production n8n stacks across Docker, managed PaaS, and on-prem environments.