To self-host n8n with Docker you run the official docker.n8n.io/n8nio/n8n image next to a PostgreSQL container, give n8n a fixed encryption key, and put a reverse proxy in front so the editor and webhooks are reachable over HTTPS. This guide walks through the whole stack on a Linux VPS, from the Compose file to the first login, and covers the mistakes that break webhooks or lock you out of your saved credentials.
n8n is a workflow automation tool: you connect triggers and nodes visually, and it runs the flows on a schedule or on incoming webhooks. Self-hosting keeps your credentials and data on your own machine instead of a vendor account.
Before you start
You need:
- A Linux VPS with at least 2 GB RAM (4 GB is more comfortable if you run many workflows). Ubuntu 22.04/24.04, Debian 12, AlmaLinux 9 and Rocky 9 all work.
- Root or sudo access over SSH.
- A domain or subdomain such as
n8n.example.comwith an A record pointing at your server IP (203.0.113.10). - Docker Engine and the Compose plugin installed.
If Docker is not installed yet, follow Deploy a Self-Hosted App with Docker Compose from Scratch first, then come back.
Warning: n8n stores the credentials for every service you connect in an encrypted database. The encryption key lives in your environment. If you lose it, those credentials cannot be decrypted — you will have to re-enter them. Generate it once and keep a copy in your password manager.
Install Docker and prepare the host
Confirm Docker is present:
docker --version
docker compose versionCreate a working directory and a dedicated environment file:
sudo mkdir -p /opt/n8n
cd /opt/n8n
openssl rand -hex 32Copy the output — that is your encryption key. Put it in /opt/n8n/.env:
sudo nano /opt/n8n/.envN8N_ENCRYPTION_KEY=paste_the_hex_string_here
POSTGRES_PASSWORD=pick_a_long_random_passwordLock the file down so only root can read it:
sudo chmod 600 /opt/n8n/.envTip: Generate the database password with openssl rand -base64 24 as well. Do not reuse the encryption key as a password.
Create the n8n Docker Compose stack
Create /opt/n8n/docker-compose.yml:
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_PROXY_HOPS=1
- N8N_SECURE_COOKIE=true
- WEBHOOK_URL=https://n8n.example.com/
- GENERIC_TIMEZONE=Europe/London
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_RUNNERS_ENABLED=true
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:A few things worth understanding here:
- PostgreSQL instead of SQLite. SQLite is the default and fine for a quick test, but Postgres handles concurrent executions and backups better. The
DB_TYPE=postgresdbblock switches n8n over. 127.0.0.1:5678:5678. The editor is bound to loopback only, so nobody can reach port 5678 directly from the internet. The reverse proxy is the only way in.depends_onwithcondition: service_healthy. n8n waits until Postgres accepts connections before starting. Without the healthcheck you get connection-refused errors on first boot.WEBHOOK_URL. Webhook nodes print this URL to the outside world. If it is wrong, every webhook you publish points at the wrong host.
Warning: Do not change N8N_ENCRYPTION_KEY after the first start. n8n will report mismatching encryption keys and your stored credentials will not open.
Start the stack and create the owner account
Bring it up:
cd /opt/n8n
sudo docker compose up -dWatch the logs until n8n reports that the editor is ready:
sudo docker compose logs -f n8nPress Ctrl+C to stop following. Then check both containers:
sudo docker compose psBoth should show running, and postgres should show healthy.
Publish the editor over HTTPS
n8n must be served over HTTPS. Two common options:
- Nginx Proxy Manager — a web UI for proxy hosts and Let's Encrypt certificates. See Nginx Proxy Manager in Docker: HTTPS for many self-hosted apps.
- Plain Nginx with Certbot — if you prefer config files.
For the second option, install Nginx and Certbot, then create /etc/nginx/sites-available/n8n:
server {
listen 80;
server_name n8n.example.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600;
}
}Enable it and get a certificate:
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/n8n
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d n8n.example.comThe Upgrade and Connection headers matter: n8n pushes execution progress to the browser over a WebSocket. Without them the editor loads but runs appear to hang.
Tip: If you keep the editor private, restrict access at the proxy with an allowlist of your office IP, or put it behind a VPN. Webhook endpoints must stay publicly reachable, so exempt the /webhook/ path from any IP restriction.
Check it works
- Open
https://n8n.example.comin a browser. You should see the n8n setup screen. - Create the owner account with your email, first name, last name and a strong password. This account is the only way into the editor, so store it in your password manager.
- Build a trivial workflow: add a Manual Trigger, then a Set node, and click Test workflow. The node should turn green and show output data.
- Confirm the database is actually in use:
sudo docker compose exec postgres psql -U n8n -d n8n -c "\dt"You should see n8n tables such as workflow_entity and credentials_entity.
- Confirm the encryption key is set inside the container:
sudo docker compose exec n8n printenv N8N_ENCRYPTION_KEYIf that returns nothing, n8n generated a key on first boot and stored it in the volume — fine, but back the volume up before you ever move the stack.
Troubleshooting
- Editor loads but executions never finish, or the UI shows a connection error. The proxy is not forwarding WebSocket upgrades. Add
proxy_http_version 1.1,proxy_set_header Upgrade $http_upgradeandproxy_set_header Connection "upgrade"to the location block, then reload Nginx. - "Mismatching encryption keys" on startup. The
N8N_ENCRYPTION_KEYvalue changed, or the.envfile is not being read. Restore the original key from your password manager and restart withsudo docker compose up -d. ECONNREFUSEDtopostgreson boot. The database container is not ready or the password in.envdiffers from the one already stored in thepostgres_datavolume. Checksudo docker compose logs postgres; if you changed the password after the first start, either revert it or remove the volume and start fresh.- Webhook URLs point to
localhostor the wrong domain.WEBHOOK_URLis missing or has a trailing slash problem. Set it tohttps://n8n.example.com/and restart the container. - 502 Bad Gateway from the proxy. The n8n container is down or not listening on 5678. Run
sudo docker compose psandsudo docker compose logs n8n, and make sure the port mapping is still127.0.0.1:5678:5678.
Next steps
- Back up the Postgres database and the n8n volume so a bad upgrade is recoverable: Back up Docker volumes with restic and restore them and Back up MySQL and PostgreSQL databases in Docker containers.
- Move secrets out of the Compose file properly: Manage secrets and environment variables in Docker Compose.
- Keep the image current without surprise breakage: Update self-hosted containers safely with Watchtower.
