Self-host n8n on a VPS with HTTPS

Run n8n in Docker on your own server, behind Caddy with a free HTTPS certificate, with its data in a volume you can back up and an easy way to update.

To self-host n8n, run its official Docker image with Docker Compose, keep its port on localhost, and put Caddy in front to serve it over HTTPS on your own domain. It takes about fifteen minutes on a fresh Ubuntu 24.04 or Debian 12 server with 2 GB of memory or more.

HTTPS isn't optional here. n8n stores the passwords and API keys your workflows use, and webhooks from other services expect an https:// address.

What you need

  • A server running Ubuntu 24.04 or Debian 12 or 13. Commands below are run as root.
  • A domain or subdomain you can point at it, like n8n.example.com.
  • Docker with the Compose plugin. If it isn't installed, Install Docker takes five minutes.

Point the domain at the server

At your DNS provider, add an A record for n8n.example.com with your server's IPv4 address, for example 203.0.113.10. Add an AAAA record for its IPv6 address too, once IPv6 works on the server. Caddy needs DNS in place to get a certificate, so do this first.

Check it from the server:

bash
getent hosts n8n.example.com

Open the web ports

Caddy needs ports 80 and 443. With ufw:

bash
ufw allow 80/tcp
ufw allow 443/tcp

n8n's own port, 5678, stays closed. Docker can open ports behind ufw's back (more on that in Install Docker), which is why the Compose file below binds it to 127.0.0.1 only.

Run n8n with Docker Compose

Make a directory for it:

bash
mkdir -p /opt/n8n
cd /opt/n8n

Create /opt/n8n/compose.yaml, with your own domain and time zone:

yaml
services:
  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      - N8N_HOST=n8n.example.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - N8N_PROXY_HOPS=1
      - NODE_ENV=production
      - N8N_WEBHOOK_URL=https://n8n.example.com/
      - WEBHOOK_URL=https://n8n.example.com/
      - GENERIC_TIMEZONE=Europe/London
      - TZ=Europe/London
    volumes:
      - n8n_data:/home/node/.n8n

volumes:
  n8n_data:

What the settings do:

  • N8N_HOST and N8N_PROTOCOL tell n8n its public address, so the links it shows you are right.
  • N8N_WEBHOOK_URL is the address it gives other services for webhooks. Without it, they get http://localhost:5678/..., which nobody outside can reach. Versions of n8n before 2.35 only read the older name, WEBHOOK_URL, so we set both; newer ones accept it too but log a deprecation warning, which you can ignore or silence by dropping that line.
  • N8N_PROXY_HOPS=1 says one proxy, Caddy, sits in front, so n8n trusts the visitor address Caddy passes on.
  • GENERIC_TIMEZONE is the time zone for schedule triggers. Use a name from timedatectl list-timezones.
  • The n8n_data volume holds everything: the database, settings, and the encryption key for your saved credentials.

Start it:

bash
docker compose up -d
docker compose logs -f

Wait for a line saying the editor is accessible, then press Ctrl+C. curl -I http://127.0.0.1:5678 should get an answer.

Put Caddy in front for HTTPS

Caddy gets and renews a Let's Encrypt certificate on its own. Install it from Caddy's repository:

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

Replace everything in /etc/caddy/Caddyfile with:

text
n8n.example.com {
    reverse_proxy 127.0.0.1:5678
}

Then:

bash
systemctl reload caddy
journalctl -u caddy -n 30

The log should show a certificate being obtained. Caddy passes websockets through without extra settings, which the n8n editor needs.

If you already run nginx on this server, use it instead of Caddy: proxy to 127.0.0.1:5678, and add the Upgrade and Connection headers for websockets. Serve a site with nginx and HTTPS covers the certificate.

Create the owner account straight away

Open https://n8n.example.com. The first person to load the page sets up the owner account, so do it now, with a strong password. Then turn on two-factor login under your user's Settings.

Back it up

Everything that matters is in the n8n_data volume. Compose names it after the directory, so it's n8n_n8n_data. To take a copy:

bash
cd /opt/n8n
docker compose stop
docker run --rm -v n8n_n8n_data:/data -v /root:/backup alpine tar czf /backup/n8n-$(date +%F).tgz -C /data .
docker compose start

The encryption key is in that archive (in the config file). Without it, a restored n8n can't decrypt your saved credentials, so never back up the database without it. It's also worth exporting your workflows to a file from time to time, one you can keep in Git:

bash
docker compose exec n8n n8n export:workflow --all --output=/home/node/.n8n/workflows.json

Copy the archives off the server. Backups you should run shows how to do it nightly.

Update

n8n releases often. Back up first, then:

bash
cd /opt/n8n
docker compose pull
docker compose up -d
docker image prune -f

Read the release notes before a major version jump. To choose when you upgrade, pin an exact version on the image, like docker.n8n.io/n8nio/n8n:2.44.3, instead of the default latest (n8n doesn't publish a tag for a whole major version), and change it when you're ready.

When it goes wrong

  • The browser shows a certificate warning or Caddy can't get one: DNS isn't pointing at the server yet, or port 80 is closed. Check getent hosts n8n.example.com and ufw status.
  • Webhooks show localhost URLs: N8N_WEBHOOK_URL is missing or the container wasn't recreated. Run docker compose up -d after any change to compose.yaml.
  • "Connection lost" in the editor: websockets aren't getting through. Caddy handles them; with nginx, check the Upgrade headers.
  • Credentials can't be decrypted after a restore: the config file with the encryption key wasn't restored with the rest.

Doing it on Rack4

The n8n one-click app installs Docker on the first boot and runs n8n with its port bound to 127.0.0.1, so nobody else can reach the owner setup page. Claim it through an SSH tunnel from your computer:

bash
ssh -L 5678:127.0.0.1:5678 root@203.0.113.10

Leave that open, browse to http://localhost:5678 in Chrome, Firefox or Edge, and create the owner account. Browsers treat localhost as secure, so n8n's secure login cookie works without HTTPS; Safari doesn't keep it.

Before you store credentials or use webhooks, move to the Compose setup above. The one-click container keeps its data in a volume called n8n_data, so tell Compose to use that one instead of making a new one, by ending compose.yaml with:

yaml
volumes:
  n8n_data:
    external: true

Then remove the old container and start the new one; your owner account and workflows come with the volume:

bash
docker rm -f n8n
cd /opt/n8n
docker compose up -d

With that change, the volume to back up is n8n_data, not n8n_n8n_data.

n8n needs at least the Standard plan, with 4 GB of memory, so it has room alongside Docker and your workflows' data. The plans page has the specs. If you want to run n8n next to other apps with a web UI to manage them, Coolify can deploy it for you.

Ready when you are

Order a server