DEV Community

Cover image for Self-Host n8n on a VPS with Docker, PostgreSQL, and Caddy (2026)
Shubham Sharma
Shubham Sharma

Posted on Originally published at techdevmantra.com

Self-Host n8n on a VPS with Docker, PostgreSQL, and Caddy (2026)

I stood this exact stack up before writing a line of it: n8n 2.36.8 talking to PostgreSQL 16.13, fronted by Caddy 2.11.4 for automatic HTTPS. Every command and version below comes from that run. If you have only used the one-line docker run for n8n, this is the production version: a real database instead of the default SQLite, HTTPS without hand-managing certificates, and a layout you can back up and upgrade without losing your workflows.

Key takeaways

  • Use PostgreSQL, not the default SQLite: set DB_TYPE=postgresdb and the DB_POSTGRESDB_* variables.
  • Set a persistent N8N_ENCRYPTION_KEY before first launch, or you lock yourself out of saved credentials on the next redeploy.
  • Let Caddy own HTTPS: point your domain at the server and it provisions a Let's Encrypt certificate automatically.
  • Verified on n8n 2.36.8, PostgreSQL 16.13, Caddy 2.11.4.

Prerequisites

  • A VPS (1 vCPU and 2 GB RAM is enough to start) on a recent Linux, with a public IP. New to running a server? Our secure VPS setup guide gets you to a safe baseline first.
  • A domain or subdomain you control: an A record for n8n.example.com pointing at the VPS.
  • Docker Engine and the Compose plugin installed. New to Docker? Our RamaLama and Docker walkthrough covers the basics on your own machine first.
  • Ports 80 and 443 open to the internet (Caddy needs them for HTTPS). Keep 5678 closed.

Heads up: n8n.example.com is a placeholder. Replace it everywhere below (in .env, the Caddyfile, and your DNS record) with a subdomain you actually own, for example n8n.yourdomain.com. example.com is a reserved documentation domain, so it will never issue a TLS certificate.

Step 1: Create the project and secrets

SSH into the VPS, make a directory, and generate two secrets: a database password and n8n's encryption key. The encryption key is the one people forget. n8n uses it to encrypt saved credentials, so if it changes between deploys, every stored credential becomes unreadable.

mkdir -p ~/n8n && cd ~/n8n
{
  echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
  echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 24)"
  echo "N8N_HOST=n8n.example.com"
} > .env
chmod 600 .env
Enter fullscreen mode Exit fullscreen mode

Swap n8n.example.com for your domain.

Step 2: Write the Compose file

Create docker-compose.yml with three services: Postgres (n8n's database), n8n, and Caddy as the HTTPS reverse proxy. Note that n8n's port 5678 is not published to the host; Caddy reaches it over the internal network, so it never faces the internet directly. Every n8n setting used below is documented in n8n's hosting docs.

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: n8n
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 5s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  n8n:
    image: n8nio/n8n:2.36.8
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_HOST: ${N8N_HOST}
      N8N_PROTOCOL: https
      N8N_PORT: 5678
      WEBHOOK_URL: https://${N8N_HOST}/
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_RUNNERS_ENABLED: "true"
      GENERIC_TIMEZONE: UTC
    volumes:
      - n8ndata:/home/node/.n8n
    restart: unless-stopped

  caddy:
    image: caddy:2-alpine
    depends_on: [n8n]
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddydata:/data
      - caddyconfig:/config
    restart: unless-stopped

volumes:
  pgdata:
  n8ndata:
  caddydata:
  caddyconfig:
Enter fullscreen mode Exit fullscreen mode

I pinned n8n to 2.36.8, the version I tested, so your deploy matches this guide. Bump it deliberately later rather than tracking latest blindly.

Step 3: Write the Caddyfile

n8n.example.com {
    reverse_proxy n8n:5678
}
Enter fullscreen mode Exit fullscreen mode

That is the entire HTTPS setup. Once your domain resolves to the server and 80 and 443 are reachable, Caddy requests a Let's Encrypt certificate on the first visit and renews it on its own. No certbot, no cron job.

Step 4: Launch and verify

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Compose starts Postgres first, waits for its healthcheck to pass, then starts n8n, which connects and runs its database migrations. Confirm both:

docker compose ps
docker compose logs -f n8n   # watch the migrations finish, then Ctrl-C
Enter fullscreen mode Exit fullscreen mode

On my run, n8n 2.36.8 came up healthy in about ten seconds once the images were cached, and the log showed it running its migration set against Postgres 16.13. That is exactly what you want to see: n8n is using Postgres, not silently falling back to SQLite. If you temporarily add ports: ["127.0.0.1:5678:5678"] to the n8n service, curl http://localhost:5678/healthz returns {"status":"ok"}. Remove it once Caddy is serving.

Open https://n8n.example.com and n8n prompts you to create the owner account. Do that immediately, before anyone else finds the URL.

The n8n owner-account setup screen on first launch

Once that is done you land on the workflow editor, ready to build your first automation:

The n8n workflow editor with the Add first step prompt

Step 5: Harden it

A few settings separate a demo from something you leave running:

  • Encryption key: you set N8N_ENCRYPTION_KEY in Step 1. Keep .env backed up somewhere safe; losing it means re-entering every credential by hand.
  • Firewall: allow only SSH, 80, and 443. On Ubuntu: ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable. Never expose 5678.
  • Restart policy: restart: unless-stopped (already in the file) brings the stack back after a reboot.
  • Stay patched: keep the host updated, and treat n8n version bumps as a deliberate step.

Step 6: Back up and upgrade

Two volumes hold your state: pgdata (workflows, executions, credentials) and n8ndata (n8n's config). Back up both.

# database dump
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > n8n-db-$(date +%F).sql.gz
# n8n data volume
docker run --rm -v n8n_n8ndata:/data -v "$PWD":/backup alpine \
  tar czf /backup/n8n-data-$(date +%F).tar.gz -C /data .
Enter fullscreen mode Exit fullscreen mode

To upgrade, change the pinned tag in docker-compose.yml, then:

docker compose pull n8n
docker compose up -d n8n
Enter fullscreen mode Exit fullscreen mode

n8n runs any new migrations on start. Because your data lives in the Postgres and n8ndata volumes, the container itself is disposable, which is the entire point of running it this way.

Common issues

  • Credentials read as broken after a redeploy. The N8N_ENCRYPTION_KEY changed. Restore the original key from your .env backup.
  • The certificate never issues. Caddy needs the domain's A record pointing at the server and ports 80 and 443 reachable. Check docker compose logs caddy for ACME errors.
  • n8n warns about task runners. n8n 2.x expects N8N_RUNNERS_ENABLED=true, which is already set above.

FAQ

Do I need PostgreSQL, or is SQLite fine? SQLite works for a hobby instance, but for anything you rely on, Postgres handles concurrent executions and backups far better. Switching later is a migration; starting on Postgres avoids it.

Can I use Nginx instead of Caddy? Yes, but Caddy's automatic HTTPS is the reason it is here: one line of config versus managing certbot and renewals.

How much server do I need? A 1 vCPU and 2 GB VPS runs a light instance. Heavy or highly concurrent workflows want more RAM and, eventually, n8n's queue mode with separate worker containers.

Final thoughts

The shape here, an application plus Postgres plus Caddy in one Compose file, is the same one you will reuse for most self-hosted tools. Set the encryption key, let Caddy own TLS, keep your state in named volumes, and upgrades become a two-line routine.

Verified end to end on n8n 2.36.8, PostgreSQL 16.13, and Caddy 2.11.4.

Top comments (0)