All tutorials
12 min read

How to Self-Host n8n with Docker Compose on Ubuntu 24.04

This n8n Docker Compose setup runs n8n 2.41.6 on Ubuntu 24.04 with PostgreSQL 18, an external task runner and Caddy HTTPS in 8 steps, plus updates and backups.

TL;DR:

  • This n8n Docker Compose stack runs n8n 2.41.6, its n8nio/runners task runner and PostgreSQL 18, with Caddy in front for HTTPS on ports 80 and 443.
  • Publish n8n on 127.0.0.1:5678 only. Docker-published ports bypass UFW, so a public 5678 stays open even when UFW denies it.
  • Set N8N_ENCRYPTION_KEY, N8N_WEBHOOK_URL and N8N_PROXY_HOPS=1. N8N_WEBHOOK_URL replaced WEBHOOK_URL in n8n 2.35.0, and n8n 1.0 removed the N8N_BASIC_AUTH variables.
  • n8n's sizing example lists 320 MB to 2 GB of memory for n8n itself. The optional n8n Assistant sandbox needs 4 GB RAM and 2 vCPU.
  • Update by changing one version line in .env. n8n 3.0 supports Docker-based installs only.
  • The Community Edition is source-available under the Sustainable Use License, which allows internal business, personal and non-commercial use.

Applies to: Ubuntu 24.04 LTS · n8n 2.41.6 · PostgreSQL 18 · Caddy 2.11.7 · Docker Compose 5.6.0 · checked October 2026

n8n is a fair-code workflow automation platform that connects apps, APIs and AI models in visual workflows started by webhooks, schedules or other triggers. This tutorial self-hosts n8n 2.41.6 on Ubuntu 24.04 with Docker Compose, PostgreSQL 18, an external task runner and Caddy for HTTPS, then covers updates and backups. A VPS gives your webhooks a public HTTPS address that stays reachable when your own computer is off.

Prerequisites and n8n VPS requirements

  • A VPS running Ubuntu 24.04 LTS or 26.04 LTS with at least 2 GB RAM, which this tutorial uses as its minimum for the three containers, or 2 vCPU and 4 GB RAM if you plan to add n8n Assistant. Docker's and Caddy's apt repositories support both releases. On Arct Cloud, Cost Optimized cvm.nano has 1 vCPU, 2 GB RAM and 25 GB NVMe, and General Purpose plans such as vm.micro have 2 vCPU, 4 GB RAM and 40 GB NVMe, the size n8n lists for n8n Assistant.
  • A non-root user with sudo privileges and SSH key login. Arct Cloud Linux images include Ubuntu 26.04 LTS and accept an SSH public key at deploy. See How to Generate an SSH Key on Windows, macOS, and Linux.
  • Docker Engine and the Docker Compose plugin from Docker's apt repository, with your user in the docker group. Follow How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04.
  • A domain with a DNS A record for a subdomain such as n8n.example.com that points to your server's IP address, for example 203.0.113.10.

Commands run as the sudo user. Replace values written in capitals, such as YOUR_DOMAIN, with your own.

How this n8n Docker Compose stack works

Four components run on the server. Only Caddy accepts connections from the internet:

ComponentRuns asPortReachable from
Caddysystemd service on the host80, 443Internet
n8nn8nio/n8n:2.41.6 container5678127.0.0.1 only
Task runnern8nio/runners:2.41.6 containernoneCompose network
PostgreSQLpostgres:18 container5432Compose network

The task runner executes Code node tasks in its own container and connects to n8n's task broker on port 5679. n8n's docs recommend this external mode for production, because internal mode runs user code next to n8n and its stored credentials. The three images are about 620 MB compressed, and n8n deletes execution data older than 14 days by default.

n8n's one-line installer at get.n8n.io uses SQLite and serves plain HTTP on port 5678. This tutorial writes the Compose file by hand for a public server.

Step 1: Open the firewall for SSH and HTTPS

Caddy needs ports 80 and 443 to get a TLS certificate, often called an SSL certificate, and to serve n8n. Allow SSH before you enable UFW, or the current session drops:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Type y if UFW asks to confirm. The output looks similar to this:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)

Do not open 5678. Step 4 publishes n8n on the loopback address, and Caddy reaches it there.

Step 2: Create the project directory and secrets

Compose reads variables from a .env file in the project directory. Create the directory:

mkdir ~/n8n
cd ~/n8n

Replace YOUR_DOMAIN with your subdomain, such as n8n.example.com, and YOUR_TIMEZONE with a time zone name, such as Europe/Berlin. Then run the block. It writes the file and generates four random secrets, and umask 077 inside the parentheses makes .env readable only by your user:

(
umask 077
cat > .env <<EOF
N8N_VERSION=2.41.6
N8N_DOMAIN=YOUR_DOMAIN
GENERIC_TIMEZONE=YOUR_TIMEZONE
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)
N8N_RUNNERS_AUTH_TOKEN=$(openssl rand -hex 32)
EOF
)

N8N_ENCRYPTION_KEY encrypts every credential n8n stores. Without it, a restored database cannot decrypt your credentials, so save a copy in your password manager.

Step 3: Add the database init script

n8n connects as a PostgreSQL user without superuser rights. This script, from n8n's withPostgres example, creates that user. Open a new file:

nano init-data.sh

Paste the script, then save with Ctrl+O and exit with Ctrl+X:

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

The PostgreSQL image runs scripts in /docker-entrypoint-initdb.d once, when the database volume is empty. A later change to the passwords in .env does not update the database.

Step 4: Write the n8n Docker Compose file

The Compose file defines the three containers, their volumes and the n8n settings:

nano compose.yaml

Paste this file, then save and exit:

services:
  postgres:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_NON_ROOT_USER: ${POSTGRES_NON_ROOT_USER}
      POSTGRES_NON_ROOT_PASSWORD: ${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:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: n8nio/n8n:${N8N_VERSION}
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    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_PROTOCOL: https
      N8N_WEBHOOK_URL: https://${N8N_DOMAIN}/
      N8N_PROXY_HOPS: "1"
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${GENERIC_TIMEZONE}
      N8N_RUNNERS_MODE: external
      N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN}
      N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
    volumes:
      - n8n_storage:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  n8n-runner:
    image: n8nio/runners:${N8N_VERSION}
    restart: unless-stopped
    environment:
      N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN}
      N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679
    depends_on:
      - n8n

volumes:
  db_storage:
  n8n_storage:

What the main settings do:

  • 127.0.0.1:5678:5678 publishes n8n on loopback only. PostgreSQL and the task broker publish no host ports.
  • N8N_WEBHOOK_URL and N8N_HOST set the public address. Without them, webhook URLs show http://localhost:5678/.
  • N8N_PROXY_HOPS: "1" trusts the X-Forwarded-* headers from one reverse proxy.
  • PGDATA keeps PostgreSQL 18 data inside the volume, since version 18 changed its default data path.
  • Both images read N8N_VERSION, because the runner and n8n versions must match.

Step 5: Start n8n with Docker Compose

Pull the images and start the stack in the background:

docker compose up -d

PostgreSQL starts first. n8n waits for its health check, creates its tables, and then starts. Check the n8n log after a minute:

docker compose logs --no-log-prefix n8n | grep -A1 "accessible via"
Editor is now accessible via:
https://n8n.example.com

If the command prints nothing, n8n is still starting. Run it again after 30 seconds.

Step 6: Set up HTTPS with a reverse proxy

The reverse proxy terminates TLS on port 443 and forwards requests to 127.0.0.1:5678. The n8n editor receives live updates over WebSockets by default, so the proxy must pass them through. Use one of the two options.

Option A: Caddy

Caddy obtains and renews TLS certificates automatically. Ubuntu 24.04 and 26.04 ship Caddy 2.6.2, so install the current release from Caddy's apt repository:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

The package starts Caddy as a systemd service named caddy. Replace the default site configuration:

sudo nano /etc/caddy/Caddyfile

Delete the existing lines and paste this, with your domain in place of YOUR_DOMAIN:

YOUR_DOMAIN {
    reverse_proxy 127.0.0.1:5678 {
        flush_interval -1
    }
}

Caddy proxies WebSockets and sets the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers by default. flush_interval -1, from n8n's own Caddyfile, sends responses to the browser without buffering. Validate the file and reload Caddy:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

The validate output ends with Valid configuration.

Option B: Nginx

If the server already runs Nginx with a Certbot certificate, as in the Nginx, PostgreSQL, and Redis guide, use this location block in the server block for YOUR_DOMAIN. Ubuntu's Nginx packages, 1.24 on 24.04 and 1.28 on 26.04, need proxy_http_version 1.1 for the WebSocket upgrade. client_max_body_size 16m raises Nginx's 1 MB default request body limit to match n8n's default 16 MiB payload limit:

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-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 3600s;
    client_max_body_size 16m;
}

Test the configuration with sudo nginx -t, then apply it with sudo systemctl reload nginx.

Step 7: Create the n8n owner account

Open https://YOUR_DOMAIN in your browser. n8n shows the owner setup form, and the first person to complete it becomes the instance owner. Until you complete this form, anyone who opens the address can claim the instance, so do this step immediately after Step 6. Enter your email address, name and a strong password.

n8n removed basic auth in version 1.0, and no supported setting turns off the login screen. Without SMTP settings, you send invite links by hand and users cannot reset their passwords.

Step 8: Verify n8n is running

List the running services:

docker compose ps --services --status running

The output lists n8n, n8n-runner and postgres. Check that n8n is connected to the database and ready:

curl -s http://127.0.0.1:5678/healthz/readiness
{"status":"ok"}

From your own computer, confirm that port 5678 is closed to the internet:

curl --max-time 5 http://YOUR_SERVER_IP:5678

Replace YOUR_SERVER_IP with the server's address, such as 203.0.113.10. The request fails or times out; a page means the port is public. Check memory use per container:

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}"

Memory use rises with large JSON or binary data, Code nodes, workflows that run at the same time, and manual executions, because n8n copies their data for the editor.

Update n8n

n8n releases a new minor version most weeks, and its docs recommend updating at least once a month. Back up first, because n8n migrates the database when a new version starts.

Find the current stable number on n8n's GitHub releases page, where beta builds are marked as pre-releases. Set it in .env, recreate the containers and remove the old images:

cd ~/n8n
sed -i 's/^N8N_VERSION=.*/N8N_VERSION=YOUR_NEW_VERSION/' .env
docker compose pull
docker compose up -d
docker image rm n8nio/n8n:YOUR_OLD_VERSION n8nio/runners:YOUR_OLD_VERSION

Replace YOUR_NEW_VERSION with the stable release number and YOUR_OLD_VERSION with the version you replaced, which grep N8N_VERSION .env shows before the change. The runner image reads the same line, and docker image rm deletes the two old images.

Read the v3.0 breaking changes before you move from 2.x to 3.0. This setup already uses Docker and an external task runner. In 3.0, the default Code node task timeout drops from 300 to 60 seconds.

postgres:18 pulls PostgreSQL minor releases. A new major version needs a dump and restore; changing the image tag alone fails with database files are incompatible with server.

Back up n8n

A complete n8n backup has two parts: the PostgreSQL database and the .n8n folder in the n8n_storage volume. Keep the .env file with them, because it holds the encryption key and the database passwords:

mkdir -p ~/n8n-backups
cd ~/n8n
STAMP=$(date +%F-%H%M)
docker compose exec -T postgres pg_dump -U postgres -d n8n -Fc > ~/n8n-backups/n8n-db-$STAMP.dump
docker compose cp n8n:/home/node/.n8n ~/n8n-backups/n8n-files-$STAMP
cp .env compose.yaml init-data.sh ~/n8n-backups/
chmod -R go-rwx ~/n8n-backups

pg_dump -Fc writes a compressed dump while n8n keeps running, and docker compose cp copies the folder out of the container. STAMP holds the date and time, so two runs on the same day write separate files.

To run the backup on a schedule, save the commands in a script such as ~/n8n/backup.sh with #!/bin/bash as the first line, make it executable with chmod +x ~/n8n/backup.sh, and call the script from cron or a systemd timer. Do not paste the commands into a crontab line, because cron treats % as a line break. Copy ~/n8n-backups to another machine, for example with rsync over SSH.

To restore the database on the same server, stop n8n and the runner, load the dump, and start them again:

docker compose stop n8n n8n-runner
docker compose exec -T postgres pg_restore -U postgres -d n8n --clean --if-exists < ~/n8n-backups/n8n-db-YOUR_TIMESTAMP.dump
docker compose start n8n n8n-runner

Replace YOUR_TIMESTAMP with the date and time in the backup file name. Never run docker compose down -v in this project: -v deletes both volumes. For server-level copies, see VPS backups.

Troubleshoot common n8n errors

SymptomCauseFix
Caddy returns 502 and logs dial tcp 127.0.0.1:5678: connect: connection refusedn8n is stopped or still startingdocker compose ps, then docker compose logs n8n
Database is not ready! in the browser, or There was an error initializing DB in the n8n logPostgreSQL is down or rejects the n8n userdocker compose logs postgres, and compare the passwords in .env
password authentication failed for user "n8n"The database volume is older than the passwords in .env, so init-data.sh did not set themOn a new install with no workflows: docker compose down, docker volume rm n8n_db_storage, then docker compose up -d
Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var.N8N_ENCRYPTION_KEY in .env changed after the first startPut the original key back in .env, then docker compose up -d
Browser certificate error on YOUR_DOMAINThe DNS A record points elsewhere, or port 80 is closedgetent hosts YOUR_DOMAIN, sudo ufw status, sudo journalctl -u caddy -n 50

FAQ

How much RAM does n8n need?

n8n's sizing example lists 320 MB to 2 GB of memory for n8n, and an idle n8n Cloud instance uses about 100 MB. PostgreSQL and the task runner add to that, and large files or Code nodes raise it. The n8n Assistant sandbox needs at least 4 GB RAM and 2 vCPU.

Is n8n free to use?

Yes, the self-hosted Community Edition is free to use under the Sustainable Use License, a fair-code license n8n created in 2022. It allows internal business, personal and non-commercial use, and n8n describes the code as source-available.

How do I update an n8n Docker Compose install?

Set N8N_VERSION in the .env file to the new release, then run docker compose pull and docker compose up -d in the project folder. The task runner image uses the same tag. Back up the database first, because n8n migrates it on start.

Should I use PostgreSQL or SQLite for n8n?

Use PostgreSQL on a server that runs workflows all day. n8n's Docker Compose guide calls SQLite fine for trying things out and recommends Postgres for production. Existing SQLite data does not move to Postgres on its own.

Why do my n8n webhook URLs show localhost:5678?

n8n builds webhook URLs from N8N_PROTOCOL, N8N_HOST and N8N_PORT unless N8N_WEBHOOK_URL is set. Behind a reverse proxy, set N8N_WEBHOOK_URL to your HTTPS address and N8N_PROXY_HOPS to 1. N8N_WEBHOOK_URL replaced WEBHOOK_URL in n8n 2.35.0.

Can I run n8n locally with Docker Desktop?

Yes. The n8n image runs under Docker Desktop and serves the editor at http://localhost:5678. Outside services cannot reach webhooks on a laptop without a tunnel, so workflows that receive webhooks run on a server with a public HTTPS address.

Next steps

Arct Cloud General Purpose plans run on AMD EPYC Milan 7003 Series processors up to 3.7 GHz with NVMe storage. Compare plans.

This work is licensed under CC BY-NC-SA 4.0.