跳到正文
DEV Community· Tunç·· 2 小時前AI 評分57

如何在 Ubuntu 24.04 上以 Docker Compose 自行託管 n8n

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

AI 導讀

n8n 2.41.6 可透過 Docker Compose 部署在 Ubuntu 24.04,並搭配 PostgreSQL 18、外部 task runner 及 Caddy 提供 HTTPS。教學將 n8n 綁定至 127.0.0.1:5678,涵蓋更新及 PostgreSQL 資料庫、.n8n 資料夾與 .env 的備份,並提醒 Docker 發佈埠可能繞過 UFW。

正文

Originally published by Arct Cloud: How to Self-Host n8n with Docker Compose on Ubuntu 24.04.

This article was prepared with AI assistance. Product references were removed and formatting adjusted for this DEV edition.

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.
  • A non-root user with sudo privileges and SSH key login. 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:

Component Runs as Port Reachable from
Caddy systemd service on the host 80, 443 Internet
n8n n8nio/n8n:2.41.6 container 5678 127.0.0.1 only
Task runner n8nio/runners:2.41.6 container none Compose network
PostgreSQL postgres:18 container 5432 Compose 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.

Troubleshoot common n8n errors

Symptom Cause Fix
Caddy returns 502 and logs dial tcp 127.0.0.1:5678: connect: connection refused n8n is stopped or still starting docker compose ps, then docker compose logs n8n
Database is not ready! in the browser, or There was an error initializing DB in the n8n log PostgreSQL is down or rejects the n8n user docker 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 them On 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 start Put the original key back in .env, then docker compose up -d
Browser certificate error on YOUR_DOMAIN The DNS A record points elsewhere, or port 80 is closed getent 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

Licensed under CC BY-NC-SA 4.0.

來源:DEV Community · dev.to