Self-Hosting n8n with Docker: Production Setup, Step by Step
Running an n8n Docker setup on your own server is not hard. Keeping it alive
for a year without losing data is the hard part.
Most n8n Docker guides give you a five-line docker run command. That works
for a demo. Then your server reboots, your workflows are gone, your webhooks
point to localhost, and your credentials will not decrypt anymore.
This guide is the other kind. You will end up with n8n running behind HTTPS,
storing data in PostgreSQL, backing itself up, and surviving restarts. It takes
about 30 minutes.
We are using n8n 2.36.x here. A lot of older n8n Docker guides are out of date,
so we will point out what changed.
Why choose an n8n Docker setup?
Three reasons come up again and again.
No execution limits. n8n Cloud charges per execution. On your own server, the only limit is your CPU. If you run a workflow every minute, that is 43,000 executions a month. On Cloud that gets expensive fast.
Your data stays yours. API keys, customer records, and payloads never leave your server. If you work with client data or handle anything under GDPR, this matters a lot.
You can reach private stuff. Internal databases, local file shares, services on your own network. No need to open ports to the internet.
The trade-off is real, so be honest about it. You are now the DevOps team. Backups, updates, and security patches are your job. If that sounds like too much, n8n Cloud is a fair deal.
What you need before you start
A server. An n8n Docker setup needs 2 vCPU and 2 GB RAM as the practical minimum. Go for 4 GB if you plan to use AI nodes or run many workflows at once. 20 GB of SSD is plenty to start. Hetzner, DigitalOcean, and Vultr all have suitable boxes for $5 to $12 a month.
A domain name. You need one to get an HTTPS certificate. A subdomain is fine. We will use n8n.example.com in this guide.
Basic terminal comfort. You should be able to SSH into a server and edit a file.
That is it. You do not need to know Docker well. The compose file does the heavy lifting.
Step 1: Prepare the server
SSH in as root and update everything first.
apt update && apt upgrade -y
Now create a normal user. Running everything as root is a bad habit.
adduser n8nadmin
usermod -aG sudo n8nadmin
Set up the firewall. Only three ports should be open.
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable
Notice that port 5678 is not open. That is n8n's default port. It will only be reachable from inside Docker, and the reverse proxy will handle traffic from the outside. This one detail blocks a whole class of attacks.
Log out and log back in as n8nadmin.
Step 2: Install Docker
Docker is the only dependency your n8n Docker setup has.
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Add your user to the docker group so you do not need sudo every time.
sudo usermod -aG docker $USER
newgrp docker
Check it worked.
docker --version
docker compose version
If both print a version number, you are good. Note that it is docker compose with a space now, not docker-compose with a hyphen. The old one is retired.
Step 3: Point your domain at the server
Go to your DNS provider and add an A record.
Type | Name | Value |
|---|---|---|
A | n8n | your.server.ip.address |
Wait a few minutes, then check it from your own machine:
dig +short n8n.example.com
It should print your server IP. Do not move on until it does. The HTTPS certificate step will fail if DNS is not ready.
Step 4: Create your project folder and secrets
On the server, make a folder for everything.
mkdir ~/n8n && cd ~/n8n
Now generate two secrets. Do this properly with random values. Do not type "password123" and promise yourself you will change it later.
# Encryption key - protects all your saved credentials
openssl rand -hex 32
# JWT secret - keeps users logged in across restarts
openssl rand -hex 32
# Database password
openssl rand -hex 16
Run each command and copy the output somewhere safe.
A serious warning about the encryption key. n8n uses N8N_ENCRYPTION_KEY to encrypt every credential you save. If you lose this key, your credentials are gone forever. Not "hard to recover" — gone. You would have to re-enter every API key and re-connect every OAuth account by hand.
Store it in a password manager right now. This is the single most common way people lose a self-hosted n8n setup.
Create the .env file:
nano .env
Paste this in and fill your own values:
# Domain
N8N_HOST=n8n.example.com
# Database
POSTGRES_USER=n8n
POSTGRES_PASSWORD=paste_your_db_password_here
POSTGRES_DB=n8n
# Secrets - never share these
N8N_ENCRYPTION_KEY=paste_your_32_byte_hex_key_here
N8N_USER_MANAGEMENT_JWT_SECRET=paste_your_other_hex_key_here
# Timezone - use your own
GENERIC_TIMEZONE=Asia/Kathmandu
Lock the file down so only you can read it.
chmod 600 .env
Step 5: Write the n8n Docker Compose file
This is the main piece of the n8n Docker setup. Three containers: PostgreSQL for data, n8n itself, and Caddy for HTTPS.
nano docker-compose.yml
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
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: 5
n8n:
image: docker.n8n.io/n8nio/n8n:2.36.6
restart: unless-stopped
environment:
# Database
- 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}
# URLs - get these wrong and webhooks break
- N8N_HOST=${N8N_HOST}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://${N8N_HOST}/
- N8N_EDITOR_BASE_URL=https://${N8N_HOST}/
- N8N_PROXY_HOPS=1
# Secrets
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_USER_MANAGEMENT_JWT_SECRET=${N8N_USER_MANAGEMENT_JWT_SECRET}
# Time
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${GENERIC_TIMEZONE}
# Security
- N8N_SECURE_COOKIE=true
- N8N_BLOCK_ENV_ACCESS_IN_NODE=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_DIAGNOSTICS_ENABLED=false
# Stop the database from growing forever
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=336
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=all
volumes:
- n8n_data:/home/node/.n8n
- ./local-files:/files
depends_on:
postgres:
condition: service_healthy
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
volumes:
postgres_data:
n8n_data:
caddy_data:
caddy_config:
Why these settings matter
PostgreSQL, not SQLite. n8n defaults to SQLite, which is fine for testing but falls over under load. Since n8n 2.0, MySQL and MariaDB are no longer supported at all, so PostgreSQL is the only real choice.
A pinned version, not latest. The tag says 2.36.6, not latest. If you use latest, a random docker compose pull can drag you across a breaking change with no warning. n8n has also started moving to stable and beta tags, and plans to drop latest in a future major version. Pinning is safer either way.
WEBHOOK_URL is not optional. Without it, n8n builds webhook URLs from the internal container hostname. Your Slack and Stripe webhooks then point to an address that does not exist. Set it once and forget about it.
N8N_PROXY_HOPS=1 tells n8n that exactly one reverse proxy sits in front of it, so it reads the real visitor IP instead of Caddy's internal one. This matters for rate limiting.

Pruning is on. EXECUTIONS_DATA_MAX_AGE=336 deletes execution history older than 14 days. Without this, a busy instance can fill a 20 GB disk in a couple of months. This is the second most common way self-hosted n8n dies.
One thing to leave out. Older guides tell you to set N8N_RUNNERS_ENABLED=true. That was correct for n8n 1.x. From version 2.0 it is deprecated, and n8n will print a warning telling you to remove it. Skip it.
Step 6: Set up Caddy for free HTTPS
Caddy sits in front of your n8n Docker container and handles HTTPS on its own.
nano Caddyfile
n8n.example.com {
reverse_proxy n8n:5678 {
flush_interval -1
}
}
That is the whole config. Change the domain to yours.
The flush_interval -1 line is small but important. n8n pushes live updates to the editor over a streaming connection. Without this, the proxy buffers those messages and your editor keeps showing "connection lost" while workflows run fine in the background. Very confusing to debug.
Step 7: Start it up
Caddy sits in front of your n8n Docker container and handles HTTPS on its own.
mkdir local-files
docker compose up -d
The first run takes a minute or two while images download and Caddy fetches your certificate.
Watch the logs:
docker compose logs -f n8n
Look for a line saying the editor is available. Press Ctrl+C to stop watching. The containers keep running.
Now open https://n8n.example.com in a browser. You should see the owner account setup screen.
Create your owner account immediately. Until you do, anyone who finds your URL can claim the instance. Do it in the first minute, not tomorrow.
Step 8: Set up backups
An n8n Docker backup has two parts. Miss either one and the restore fails. Miss either one and the restore fails.
The PostgreSQL database — your workflows, credentials, and history.
The n8n data volume — settings and any custom nodes.
Create a backup script:
nano ~/n8n/backup.sh
#!/bin/bash
set -e
BACKUP_DIR=~/n8n-backups
DATE=$(date +%Y-%m-%d_%H-%M)
mkdir -p $BACKUP_DIR
cd ~/n8n
# Database
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > "$BACKUP_DIR/db_$DATE.sql.gz"
# n8n data volume
docker run --rm \
-v n8n_n8n_data:/data:ro \
-v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/n8ndata_$DATE.tar.gz" -C /data .
# Keep 14 days
find $BACKUP_DIR -name "*.gz" -mtime +14 -delete
echo "Backup done: $DATE"
Make it runnable and schedule it:
chmod +x ~/n8n/backup.sh
crontab -e
Add this line to run it every night at 3 AM:
0 3 * * * /home/n8nadmin/n8n/backup.sh >> /home/n8nadmin/backup.log 2>&1
Check the volume name matches with docker volume ls. Docker Compose prefixes volumes with the folder name, so n8n_data inside a folder called n8n becomes n8n_n8n_data.
Copy backups off the server. A backup sitting on the same disk as the thing it is backing up is not a backup. Push them to S3, Backblaze, or even another VPS with rclone.
Step 9: Update your n8n Docker setup safely
Updating is three commands. Do the backup first, every time.
cd ~/n8n
./backup.sh
# Edit docker-compose.yml and change the version tag
nano docker-compose.yml
docker compose pull
docker compose down
docker compose up -d
n8n runs its own database migrations on startup. You do not need to touch the schema.
Two habits worth keeping:
Read the release notes before crossing a major version. n8n publishes a breaking-changes page for each major release.
Use the Migration Report. Since version 1.121.0 there is a tool at Settings → Migration Report that scans your workflows and tells you exactly what will break. Run it before any big jump.
Also worth knowing: n8n 3.0 will drop npm and npx installs entirely. Docker will be the only supported way to self-host. If you are reading this, you are already on the right path.
Step 10: Scale your n8n Docker setup with queue mode
The setup above runs everything in one container. That is fine up to a point.
If your workflows start queuing up or timing out, switch to queue mode. This splits n8n into a main process that handles the UI and webhooks, plus worker containers that actually run the workflows. Redis passes jobs between them.
Add Redis and a worker to your compose file:
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
n8n-worker:
image: docker.n8n.io/n8nio/n8n:2.36.6
restart: unless-stopped
command: worker
environment:
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
depends_on:
- redis
- postgres
Then add these to your main n8n service too:
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
Scale workers up and down as needed:
docker compose up -d --scale n8n-worker=3
The critical rule: every container must share the same N8N_ENCRYPTION_KEY and the same database. If a worker has a different key, it cannot decrypt credentials and every workflow fails with a confusing error.
Do not start here. Run single-container mode until you actually feel the pain. Queue mode adds moving parts, and moving parts break.
Common n8n Docker problems and how to fix them
"My webhook URL says localhost." WEBHOOK_URL is missing or wrong. Set it to your real HTTPS domain with a trailing slash, then restart.
"Permissions 0644 for n8n settings file are too wide." Set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true and restart. n8n will fix the file itself.
"All my credentials broke after a restart." Your encryption key changed. This happens when the key was auto-generated inside the container and the volume got recreated. Restore from backup, then set N8N_ENCRYPTION_KEY explicitly and never change it again.
"The editor keeps saying connection lost." Missing flush_interval -1 in your Caddyfile, or proxy buffering if you use Nginx. With Nginx you need proxy_buffering off; and a long proxy_read_timeout.
"The server ran out of memory." Usually a big payload or a runaway loop. Check that pruning is on, add 2 GB of swap, and set a memory limit on the n8n container so one bad workflow cannot take the whole box down.
"The disk is full." Execution history. Turn on EXECUTIONS_DATA_PRUNE, lower EXECUTIONS_DATA_MAX_AGE, and consider setting EXECUTIONS_DATA_SAVE_ON_SUCCESS=none if you only care about failures.
n8n Docker security checklist
Run through this before you put anything real on the instance.
[ ] Firewall open on 22, 80, 443 only. Port 5678 is closed.
[ ] Owner account created with a strong password.
[ ] Two-factor auth turned on in Settings.
[ ]
N8N_ENCRYPTION_KEYsaved in a password manager.[ ]
N8N_SECURE_COOKIE=true.[ ]
N8N_BLOCK_ENV_ACCESS_IN_NODE=trueso Code nodes cannot read your server environment.[ ] Public API turned off if you do not use it (
N8N_PUBLIC_API_DISABLED=true).[ ]
.envfile set tochmod 600.[ ] Backups running and tested with an actual restore.
[ ] Unattended security upgrades enabled on the host.
That last one deserves a note. A backup you have never restored is a guess, not a backup. Spin up a throwaway VPS once, restore into it, and confirm your workflows are there.
Wrapping up
You now have n8n running on your own server with HTTPS, a real database, automatic backups, and sane security defaults. Running cost is somewhere between $5 and $12 a month, with no cap on executions.
The three things that will actually save you later:
Guard the encryption key. Everything else is recoverable. This is not.
Keep pruning on. Disk space disappears quietly.
Pin your version and read release notes. Surprise upgrades are how production breaks at 2 AM.
Start with the single-container setup. Add queue mode when your workflows tell you it is time, not before.
Now that the server is ready, the next question is what to run on it. If you are moving off a paid tool, we broke down the real numbers in why startups are leaving Zapier for n8n — self-hosting is exactly what makes that switch worth it. And if you want to go further than simple triggers, our guide to building autonomous systems with the n8n AI Agent node picks up where this one stops. Just remember that AI nodes are memory-hungry, so give your box 4 GB before you start.
Useful links
Enjoyed this article?
Subscribe to get notified when we publish new articles like this one.



