# 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.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-debian) first.
- A non-root user with `sudo` rights and SSH key login, set up as in [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/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](/guides/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:

```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.

> **Warning**
>
> Back up `.env` now and keep a copy off the server. If `N8N_ENCRYPTION_KEY` is lost, restored workflows load but every saved credential becomes unreadable. Never change the key on an instance that already holds credentials.

## 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](/guides/nginx-reverse-proxy-certbot) and [Traefik](/guides/traefik-reverse-proxy).

## 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.

> **Note**
>
> Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

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
      - N8N_SMTP_USER=n8n@example.com
      - N8N_SMTP_PASS=${N8N_SMTP_PASS}
      - N8N_SMTP_SENDER=n8n@example.com
```

> **Tip**
>
> Locked out without SMTP? `docker compose exec -u node n8n n8n user-management:reset` returns user management to its pre-setup state so you can create a new owner. It removes all user accounts, so back up first; workflows and credentials stay in the database.

## 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

- Learn the Compose file format in depth with [Docker Compose basics](/guides/docker-compose-basics).
- Compare n8n with [Node-RED](/guides/install-node-red) and [Activepieces](/guides/install-activepieces) for your automation needs.
- Find servers sized for automation workloads on the [n8n hosting](/n8n-hosting) page.
- Read n8n's [hosting documentation](https://docs.n8n.io/hosting/) 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.

---

Source: <https://hyperdc.com/guides/tutorials/install-n8n>\
Updated: 2026-10-09
