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

Source: https://vpsnine.com/help/self-host-n8n · Updated: 2026-10-16

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](https://vpsnine.com/help/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](https://vpsnine.com/help/ipv6-setup). 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](https://vpsnine.com/help/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](https://vpsnine.com/help/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](https://vpsnine.com/help/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 VPSNine

The [n8n one-click app](https://vpsnine.com/apps/n8n) installs Docker and runs n8n with its data in a named volume on the first boot. You still add your domain and HTTPS, as above, before storing credentials in it.

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](https://vpsnine.com/plans) has the specs. If you want to run n8n next to other apps with a web UI to manage them, [Coolify](https://vpsnine.com/help/deploy-apps-with-coolify) can deploy it for you.
