How to install n8n with Docker Compose, PostgreSQL and HTTPS
Self-host n8n with Docker Compose, PostgreSQL and the official task runner behind Caddy HTTPS, then add queue mode workers, backups and safe updates.
- Intermediate
- 40 min read
- Updated
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13
This guide is not available in your language yet, so it is shown in English.
On this page
- Prerequisites
- Step 1 — Create the project folder and secrets
- Step 2 — Add the database initialisation script
- Step 3 — Write the Compose file
- Step 4 — Start n8n
- Step 5 — Publish n8n over HTTPS with Caddy
- Step 6 — Create the owner account
- Step 7 — Scale out with queue mode (optional)
- Back up and restore
- Update n8n
- Troubleshooting
- Webhook URLs show http://localhost:5678
- EACCES: permission denied on /home/node/.n8n
- Mismatching encryption keys
- Code node executions fail or time out
- FATAL: database files are incompatible with server
- Next steps
n8n is a workflow automation platform. You connect apps, APIs and AI models on a visual canvas, react to webhooks and schedules, and drop into JavaScript or Python when a node does not do exactly what you need. Running it on your own server keeps workflows, credentials and execution data under your control.
This guide installs n8n with Docker Compose, following the examples in n8n's official n8n-hosting repository: n8n with PostgreSQL, plus the official task-runner sidecar that executes Code node scripts outside the main process. n8n listens only on 127.0.0.1, and Caddy publishes it over HTTPS on your domain. You then create the owner account, learn when and how to switch to queue mode with Redis and worker containers, and set up backups, restores and updates.
Prerequisites
- A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13 with Docker Engine and the Compose plugin. Follow Install Docker on Ubuntu or Install Docker on Debian first.
- A non-root user with
sudorights and SSH key login, set up as in Secure a new Linux server and Set up SSH keys. - A subdomain such as
n8n.example.comwith an A record (and an AAAA record if you use IPv6) pointing at the server. - Caddy installed on the host as described in Caddy reverse proxy. Nginx or Traefik work too; the n8n side of the setup is the same.
n8n does not publish minimum hardware for a Docker Compose install with PostgreSQL. Its newer all-in-one Compose stack, which adds AI sandbox services, asks for at least 2 vCPUs and 4 GB of RAM; the stack in this guide is lighter. Memory use grows with the number of parallel executions and with the size of the data each workflow moves around, so treat the figures below as a conservative starting point.
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 2 vCPU |
| RAM | Not published | 2 GB, or 4 GB with queue mode |
| Disk | Not published | 20 GB SSD, more if you keep a long execution history |
Step 1 — Create the project folder and secrets
Keep everything for n8n in /opt/n8n, owned by your admin user:
sudo mkdir -p /opt/n8n
sudo chown $USER:$USER /opt/n8n
cd /opt/n8nCreate the .env file. Docker Compose reads it automatically, and the openssl calls fill in long random secrets while the file is written:
cat > .env <<EOF
N8N_VERSION=2.42.5
N8N_DOMAIN=n8n.example.com
GENERIC_TIMEZONE=Europe/Istanbul
POSTGRES_USER=postgres
POSTGRES_PASSWORD=$(openssl rand -hex 24)
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n
POSTGRES_NON_ROOT_PASSWORD=$(openssl rand -hex 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
RUNNERS_AUTH_TOKEN=$(openssl rand -hex 32)
EOF
chmod 600 .envWhat the values do:
N8N_VERSIONpins the n8n and task-runner images to one release.2.42.5was the current stable release on 2026-10-09; check the stable version on n8n's Docker page or in the release notes and use that. Pinning means updates only happen when you change this line.N8N_DOMAINis the public host name, andGENERIC_TIMEZONEis the timezone that Schedule triggers use. Use your own IANA timezone name, such asEurope/BerlinorAmerica/New_York.- The
POSTGRES_*values create the database. n8n connects with the separate, non-superuser accountn8n, as in n8n's official example. N8N_ENCRYPTION_KEYencrypts every credential n8n stores.RUNNERS_AUTH_TOKENis the shared secret between n8n and its task runner.
Step 2 — Add the database initialisation script
The official example mounts a small script into PostgreSQL's /docker-entrypoint-initdb.d folder. It runs once, when the database volume is empty, and creates the n8n user with rights limited to the n8n database:
cat > /opt/n8n/init-data.sh <<'EOF'
#!/bin/bash
set -e
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No Environment variables given!"
fi
EOF
chmod +x /opt/n8n/init-data.shThe quoted 'EOF' keeps your shell from expanding the variables; PostgreSQL's container fills them in from its environment at first start.
Step 3 — Write the Compose file
Create /opt/n8n/compose.yaml. It is n8n's official withPostgres example with three changes: the port is published on 127.0.0.1 only, the public URL settings for a reverse proxy are added, and the encryption key comes from .env:
services:
postgres:
image: postgres:18
restart: always
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
- PGDATA=/var/lib/postgresql/data
volumes:
- db_storage:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=${N8N_DOMAIN}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_WEBHOOK_URL=https://${N8N_DOMAIN}/
- N8N_PROXY_HOPS=1
- NODE_ENV=production
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${GENERIC_TIMEZONE}
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_storage:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
n8n-runner:
image: n8nio/runners:${N8N_VERSION}
restart: always
environment:
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
depends_on:
- n8n
volumes:
db_storage:
n8n_storage:A few settings deserve an explanation:
PGDATAis pinned because thepostgres:18image moved its default data directory. n8n's example keeps the old path so the volume mount stays stable across major versions. Do not remove that line.- The
n8n_storagevolume holds/home/node/.n8n. Even with PostgreSQL, n8n keeps its settings file and encryption key there, so the volume must persist. N8N_WEBHOOK_URLandN8N_PROXY_HOPS=1tell n8n that it sits behind one reverse proxy and which public address to show for webhooks.N8N_WEBHOOK_URLreplaced the olderWEBHOOK_URLname in n8n 2.35.0; the old name still works but logs a deprecation warning.N8N_RUNNERS_MODE=externalruns Code node scripts in the separaten8nio/runnerscontainer instead of a child process of n8n. n8n's documentation recommends external task runners for production, and the runner image version must always match the n8n version.
Step 4 — Start n8n
Pull the images and start the stack in the background:
cd /opt/n8n
docker compose pull
docker compose up -d
docker compose psAll three services should show running, and postgres should report healthy. Follow the n8n log until it reports that the editor is accessible, then press Ctrl+C:
docker compose logs -f n8nCheck the health endpoint from the server itself:
curl -s http://127.0.0.1:5678/healthzYou should see {"status":"ok"}. If the runner cannot connect, docker compose logs n8n-runner shows the reason, usually a token or version mismatch.
Step 5 — Publish n8n over HTTPS with Caddy
Add a site block for your subdomain to /etc/caddy/Caddyfile. The flush_interval -1 line comes from n8n's own Caddy example and sends streamed responses to the browser immediately:
n8n.example.com {
reverse_proxy 127.0.0.1:5678 {
flush_interval -1
}
}Reload Caddy and make sure the firewall only allows SSH and web traffic. Port 5678 must not be opened, because n8n is only reachable through Caddy:
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
curl -I https://n8n.example.comThe last command should return HTTP/2 200 with a valid certificate. Caddy obtains and renews the certificate automatically and passes the X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto headers that n8n expects. For Nginx or Traefik instead of Caddy, see Nginx with Certbot and Traefik.
Step 6 — Create the owner account
Open https://n8n.example.com right away. On first launch n8n shows a sign-up form for the owner account, and whoever fills it in first becomes the instance owner. Use a password of at least eight characters with a number and a capital letter, ideally generated by a password manager.
Without an SMTP server, n8n cannot email invitations or password-reset links; you copy invite links by hand instead.
To enable email, add these lines to the environment list of the n8n service, put N8N_SMTP_PASS in .env, and run docker compose up -d to recreate the container. N8N_SMTP_SSL defaults to true, so set it to false for port 587 and keep N8N_SMTP_STARTTLS on:
- N8N_EMAIL_MODE=smtp
- N8N_SMTP_HOST=smtp.example.com
- N8N_SMTP_PORT=587
- N8N_SMTP_SSL=false
- N8N_SMTP_STARTTLS=true
- [email protected]
- N8N_SMTP_PASS=${N8N_SMTP_PASS}
- [email protected]Step 7 — Scale out with queue mode (optional)
In the default mode a single n8n process receives webhooks, runs the editor and executes every workflow. In queue mode the main instance only accepts triggers and webhooks, writes each execution to the database and puts its ID into a Redis queue. Separate worker processes pick up the IDs, run the workflows and write the results back.
You need queue mode when executions pile up, when heavy workflows make the editor or webhooks slow, or when you want to add capacity by running more workers. The requirements, all from n8n's documentation, are:
- PostgreSQL (queue mode with SQLite is not supported for distributed setups) and a Redis instance that the main process and all workers can reach.
- The same
N8N_ENCRYPTION_KEYon the main instance and every worker, otherwise workers cannot decrypt credentials. - The same n8n version on every process.
- Queue mode does not support filesystem storage for binary data, so check n8n's binary data settings before you run workflows that handle large files.
Back up first (see the next section), then replace compose.yaml with this version. It follows n8n's official withPostgresAndWorker example: a YAML anchor shares the configuration between the main instance and the worker, and each of them gets its own task runner:
x-n8n: &n8n-common
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
- N8N_HOST=${N8N_DOMAIN}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_WEBHOOK_URL=https://${N8N_DOMAIN}/
- N8N_PROXY_HOPS=1
- NODE_ENV=production
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${GENERIC_TIMEZONE}
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
volumes:
- n8n_storage:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
services:
postgres:
image: postgres:18
restart: always
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
- PGDATA=/var/lib/postgresql/data
volumes:
- db_storage:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
restart: always
volumes:
- redis_storage:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 5s
retries: 10
n8n:
<<: *n8n-common
ports:
- "127.0.0.1:5678:5678"
n8n-runner:
image: n8nio/runners:${N8N_VERSION}
restart: always
environment:
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
depends_on:
- n8n
n8n-worker:
<<: *n8n-common
command: worker --concurrency=5
depends_on:
- n8n
n8n-worker-runner:
image: n8nio/runners:${N8N_VERSION}
restart: always
environment:
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-worker:5679
depends_on:
- n8n-worker
volumes:
db_storage:
n8n_storage:
redis_storage:Apply the change and check that the worker connects to Redis and the database:
cd /opt/n8n
docker compose up -d --remove-orphans
docker compose ps
docker compose logs --tail=50 n8n-workerRedis is only reachable on the internal Compose network and is not published on the host. Each worker runs up to 10 jobs at once by default; --concurrency=5 lowers that, and n8n advises against values below 5 because many workers with low concurrency can exhaust the database connection pool. To add capacity, raise the concurrency or add another worker and runner pair under new service names. For very high webhook traffic, n8n also offers dedicated webhook processes (n8n webhook) behind a load balancer, described in its queue mode documentation.
Back up and restore
Three things hold n8n's state: the PostgreSQL database (workflows, credentials, execution history), the n8n_storage volume (settings file, encryption key, community nodes) and the files in /opt/n8n (.env, compose.yaml, init-data.sh). Back up all three:
sudo mkdir -p /opt/backups
sudo chown $USER:$USER /opt/backups
cd /opt/n8n
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"' > /opt/backups/n8n-db-$(date +%F).dump
docker run --rm -v n8n_n8n_storage:/data:ro -v /opt/backups:/backup alpine tar czf /backup/n8n-storage-$(date +%F).tar.gz -C /data .
tar czf /opt/backups/n8n-config-$(date +%F).tar.gz -C /opt/n8n .env compose.yaml init-data.sh
chmod 600 /opt/backups/n8n-*Compose names volumes after the project folder, so the storage volume is n8n_n8n_storage; confirm with docker volume ls. In addition, n8n's CLI can export workflows and credentials as JSON files, which are handy for moving single workflows between instances. --backup is shorthand for --all --pretty --separate, and the credentials stay encrypted with your key:
docker compose exec -u node n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/workflows/
docker compose exec -u node n8n n8n export:credentials --backup --output=/home/node/.n8n/backups/credentials/
docker compose cp n8n:/home/node/.n8n/backups /opt/backups/n8n-export-$(date +%F)To restore on a new server, install Docker, unpack the config archive into /opt/n8n so that the original .env is in place, then restore the database before n8n starts for the first time:
cd /opt/n8n
docker compose up -d --wait postgres
docker compose exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < /opt/backups/n8n-db-2026-10-09.dump
docker compose create n8n
docker run --rm -v n8n_n8n_storage:/data -v /opt/backups:/backup alpine tar xzf /backup/n8n-storage-2026-10-09.tar.gz -C /data
docker compose up -d--wait returns once PostgreSQL is healthy and the initialisation script has created the n8n user, which the restored tables belong to. Replace the dates with the ones in your file names. Copy /opt/backups to another machine or object storage regularly, for example with rsync or restic; a backup that only lives on the same server does not survive the loss of that server.
Update n8n
n8n ships frequent releases and recommends updating at least once a month, so you never have to jump many versions at once. Before each update:
- Read the release notes. Before a major version, also read its breaking-changes page; n8n 2.0, for example, blocked environment access in the Code node and disabled the Execute Command node by default.
- Back up the database, the storage volume and
.envas shown above.
Then change N8N_VERSION in .env to the new release and recreate the containers. Both the n8n and runner images follow the same variable:
cd /opt/n8n
nano .env
docker compose pull
docker compose up -d
docker compose exec n8n n8n --versionDatabase migrations run automatically when n8n starts. Do not change the postgres:18 image to a new major version as part of a routine update: a PostgreSQL major upgrade needs a dump and restore, as n8n's hosting repository explains.
Troubleshooting
Webhook URLs show http://localhost:5678
n8n does not know its public address. Check that N8N_WEBHOOK_URL is https://n8n.example.com/ (with a trailing slash), that N8N_PROXY_HOPS=1 is set and that the domain in .env is correct, then run docker compose up -d. Webhooks registered with external services before the fix must be registered again, usually by deactivating and reactivating the workflow.
EACCES: permission denied on /home/node/.n8n
The n8n container runs as the unprivileged node user (UID 1000). This error appears when you replace the named volume with a host folder that belongs to root. Give the folder to UID 1000, for example sudo chown -R 1000:1000 /opt/n8n/n8n-data, or keep the named volume used in this guide. n8n 2.x also expects its settings file to have 0600 permissions; docker compose exec n8n chmod 600 /home/node/.n8n/config fixes a file that was copied in with looser permissions.
Mismatching encryption keys
The key in N8N_ENCRYPTION_KEY differs from the key stored in the settings file inside n8n_storage, typically after a restore with a new .env. Put the original key back into .env and run docker compose up -d. If the original key is lost, the stored credentials cannot be recovered and must be recreated.
Code node executions fail or time out
The task runner is not connected. Run docker compose logs n8n-runner: the runner and n8n images must use the same version, RUNNERS_AUTH_TOKEN must be identical in both services, and the n8n service must keep N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0 so the runner container can reach port 5679.
FATAL: database files are incompatible with server
The PostgreSQL image was changed to a newer major version than the one that created the data directory. Nothing is deleted: set the image back to the previous major version (for example postgres:16), start again, and follow the dump-and-restore upgrade path in n8n's hosting repository if you want to move to the new version.
Next steps
- Learn the Compose file format in depth with Docker Compose basics.
- Compare n8n with Node-RED and Activepieces for your automation needs.
- Find servers sized for automation workloads on the n8n hosting page.
- Read n8n's hosting documentation for environment variables, scaling and security settings.
Frequently asked questions
Do I need PostgreSQL, or is SQLite enough for n8n?
n8n uses SQLite by default, which suits tests and small single-instance setups. n8n's own hosting examples recommend PostgreSQL for production, and queue mode needs it. Starting with PostgreSQL means you never have to migrate later.
What happens if I lose the n8n encryption key?
n8n encrypts every saved credential with N8N_ENCRYPTION_KEY. With a different key, a restored database still shows your workflows, but the stored credentials cannot be decrypted and must be entered again. Keep the .env file in every backup.
When do I need n8n queue mode?
Switch to queue mode when a single n8n process can no longer keep up with your executions, or when long-running workflows slow down the editor and webhooks. It adds Redis and separate worker containers. A single instance is enough for most small teams.
Why do my n8n webhook URLs show localhost:5678?
n8n builds webhook URLs from its internal host and port unless you give it the public address. Set N8N_WEBHOOK_URL (WEBHOOK_URL on older releases) and N8N_PROXY_HOPS=1, then recreate the container with docker compose up -d.
Can I run this on a HyperDC server?
Yes. The steps work on a HyperDC Linux VPS, VDS or dedicated server with root access running Ubuntu 24.04, Ubuntu 26.04, Debian 12 or Debian 13. Size the server for the number of workflows you plan to run in parallel.