Skip to content

TutorialsAutomation

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
  1. Prerequisites
  2. Step 1 — Create the project folder and secrets
  3. Step 2 — Add the database initialisation script
  4. Step 3 — Write the Compose file
  5. Step 4 — Start n8n
  6. Step 5 — Publish n8n over HTTPS with Caddy
  7. Step 6 — Create the owner account
  8. Step 7 — Scale out with queue mode (optional)
  9. Back up and restore
  10. Update n8n
  11. Troubleshooting
  12. Webhook URLs show http://localhost:5678
  13. EACCES: permission denied on /home/node/.n8n
  14. Mismatching encryption keys
  15. Code node executions fail or time out
  16. FATAL: database files are incompatible with server
  17. 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 sudo rights and SSH key login, set up as in Secure a new Linux server and Set up SSH keys.
  • A subdomain such as n8n.example.com with 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.

ResourceMinimum (official)Suggested starting point
CPUNot published2 vCPU
RAMNot published2 GB, or 4 GB with queue mode
DiskNot published20 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:

Bash
sudo mkdir -p /opt/n8n
sudo chown $USER:$USER /opt/n8n
cd /opt/n8n

Create the .env file. Docker Compose reads it automatically, and the openssl calls fill in long random secrets while the file is written:

Bash
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 .env

What the values do:

  • N8N_VERSION pins the n8n and task-runner images to one release. 2.42.5 was 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_DOMAIN is the public host name, and GENERIC_TIMEZONE is the timezone that Schedule triggers use. Use your own IANA timezone name, such as Europe/Berlin or America/New_York.
  • The POSTGRES_* values create the database. n8n connects with the separate, non-superuser account n8n, as in n8n's official example.
  • N8N_ENCRYPTION_KEY encrypts every credential n8n stores. RUNNERS_AUTH_TOKEN is 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:

Bash
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.sh

The 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:

YAML
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:

  • PGDATA is pinned because the postgres:18 image 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_storage volume holds /home/node/.n8n. Even with PostgreSQL, n8n keeps its settings file and encryption key there, so the volume must persist.
  • N8N_WEBHOOK_URL and N8N_PROXY_HOPS=1 tell n8n that it sits behind one reverse proxy and which public address to show for webhooks. N8N_WEBHOOK_URL replaced the older WEBHOOK_URL name in n8n 2.35.0; the old name still works but logs a deprecation warning.
  • N8N_RUNNERS_MODE=external runs Code node scripts in the separate n8nio/runners container 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:

Bash
cd /opt/n8n
docker compose pull
docker compose up -d
docker compose ps

All 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:

Bash
docker compose logs -f n8n

Check the health endpoint from the server itself:

Bash
curl -s http://127.0.0.1:5678/healthz

You 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:

Caddyfile
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:

Bash
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.com

The 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:

YAML
      - 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_KEY on 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:

YAML
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:

Bash
cd /opt/n8n
docker compose up -d --remove-orphans
docker compose ps
docker compose logs --tail=50 n8n-worker

Redis 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:

Bash
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:

Bash
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:

Bash
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:

  1. 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.
  2. Back up the database, the storage volume and .env as 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:

Bash
cd /opt/n8n
nano .env
docker compose pull
docker compose up -d
docker compose exec n8n n8n --version

Database 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

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.

Sources

Wachtwoord genereren

Please confirm