Skip to content

Command Palette

Search for a command to run...

How to Deploy n8n on a VPS

Updated Jul 2026

Self-host n8n on your own VPS — a fair-code Zapier/Make alternative for workflow automation, running on Docker with Postgres and HTTPS.

Before you start
  • A VPS with 1–2 vCPU / 2 GB RAM (n8n plus a Postgres database)
  • A fresh Ubuntu 24.04 server with root/sudo SSH access, Docker + Compose installed
  • A domain you can point at the server — webhooks and OAuth callbacks need a public HTTPS URL

What n8n is

n8n is a fair-code, self-hostable workflow-automation tool — think Zapier or Make, but running on a box you own. You build automations in a visual node editor: a trigger fires (a webhook, a schedule, an app event), data flows through a chain of nodes, and each node transforms it, calls an API, or writes it somewhere. There are 400+ built-in integrations, and when none fits you drop in a Code node and write JavaScript or Python directly.

The appeal is the same one that draws people to any self-hosted tool: cost and control. Zapier and Make bill per task or per operation, and a busy automation that runs thousands of times a month gets expensive fast. A single VPS running n8n costs the same whether it fires a hundred workflows a day or a hundred thousand — you're bounded by the box, not a per-task meter. You also keep your data and credentials on infrastructure you own, which matters when your workflows touch internal systems or customer records.

One thing to get right before you start: n8n really wants a real domain and public HTTPS. Many integrations rely on OAuth callbacks, and inbound webhooks have to be reachable from the internet. That's the single biggest reason a VPS beats a home lab for n8n — a stable public IP and a domain you control, rather than fighting NAT and dynamic addresses. This guide covers the production setup: Docker Compose, Postgres as the datastore, and a reverse proxy for TLS.

Server sizing — why 2 GB is plenty

n8n itself is light. The core process is a single Node service, and unless you're running heavy parallel workflows it sits mostly idle, waking to execute a workflow and going back to sleep. 1–2 vCPU and 2 GB RAM comfortably runs a solo or small-team instance plus its Postgres database, with headroom for the occasional memory-hungry workflow that pulls a large dataset through a node.

This makes n8n one of the cheapest useful things you can self-host. A small box is the right start — you do not need to over-provision. Where it gets heavier is high-volume parallel execution: if you're running many workflows concurrently, n8n supports a queue mode that offloads executions to separate worker containers backed by Redis, so you can scale horizontally by adding workers. That's a real capability, but it's beyond a first deploy and you'll know when you need it (executions backing up, the main process pegged). Start on a single small box in the default execution mode; reach for queue mode later if throughput demands it.

For the initial deploy, a Hetzner CX22 (2 vCPU / 4 GB) has ample room, or the smallest tier that gives you 2 GB is enough. Give the disk a little thought too: execution data accumulates in Postgres over time, so 20–40 GB is comfortable, and you can prune old execution history to keep it in check.

Prepare the server

This guide assumes you've already worked through Docker & Compose on Ubuntu — a fresh Ubuntu 24.04 box, a non-root deploy user, Docker Engine and the Compose plugin installed, and ufw allowing SSH plus 80 and 443. If you haven't, do that first; everything below is a compose file on top of that base.

n8n listens on port 5678 by default, but you should not open that port to the internet. A reverse proxy will terminate TLS on 443 and forward to n8n over the internal Docker network, so 5678 stays private. With the base firewall from the Docker guide (SSH, 80, 443), you already have exactly the right ports open and nothing more.

Create a working directory for the stack:

mkdir ~/n8n && cd ~/n8n

Install n8n

n8n runs as two containers: the official n8nio/n8n image, and a Postgres database. n8n defaults to SQLite, which is fine for a quick trial, but Postgres is the production-recommended datastore — SQLite serializes writes and struggles under concurrent executions and larger volumes of execution data, whereas Postgres handles concurrency and stays reliable as your history grows. Set it up on Postgres from the start; migrating later is more work than doing it right once.

First, generate an encryption key and a database password, and write them to a .env file the compose stack will read:

cat > .env <<EOF
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .env

Back up that .env file somewhere safe right now. The N8N_ENCRYPTION_KEY is the most important secret in the whole setup — see the warning below. Then create the compose file:

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

  n8n:
    image: n8nio/n8n:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      # --- database ---
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      # --- credential encryption ---
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      # --- public address (edit to your domain) ---
      N8N_HOST: n8n.example.com
      N8N_PROTOCOL: https
      N8N_PORT: 5678
      WEBHOOK_URL: https://n8n.example.com/
      # --- scheduling ---
      GENERIC_TIMEZONE: Europe/Berlin
      TZ: Europe/Berlin
    volumes:
      - n8n_data:/home/node/.n8n

volumes:
  postgres_data:
  n8n_data:

A few things worth understanding before you bring it up:

  • ports: "127.0.0.1:5678:5678" binds n8n only to localhost, so it's reachable by the reverse proxy on the same box but never directly from the internet. Note that Docker can bypass ufw for published ports, so this explicit 127.0.0.1 bind — not the firewall — is what keeps 5678 private.
  • The n8n_data volume (/home/node/.n8n) holds n8n's config and internal state. Combined with Postgres, these two volumes are your entire installation.
  • GENERIC_TIMEZONE and TZ set the timezone n8n uses for cron/schedule triggers. Get this wrong and your scheduled workflows fire at the wrong hour — set both to your actual timezone.

Edit the domain and timezone values, then start the stack:

docker compose up -d
docker compose logs -f n8n

On first start n8n runs its database migrations against Postgres automatically, so the first boot takes a little longer. Watch the log until it reports the editor is ready on port 5678.

The encryption key — read this

N8N_ENCRYPTION_KEY encrypts every credential you save in n8n (API keys, OAuth tokens, database passwords) at rest. If you don't set it, n8n generates one on first run and stores it inside the .n8n volume — which works, but means the key lives only in one place. Set it explicitly, as above, so it's in your .env and your backups.

If you ever lose this key, every stored credential becomes unrecoverable. n8n won't be able to decrypt them, and you'll have to delete and re-enter every credential by hand. Treat the key like a root password: back it up, and never change it on an existing instance unless you're deliberately resetting all credentials.

HTTPS + domain

n8n needs to be served over HTTPS on a real domain — this is not optional. OAuth providers refuse to redirect to a plain-HTTP or IP-based callback, and external services that call your webhooks expect a proper TLS endpoint. Put a reverse proxy in front to terminate TLS and forward to n8n on 5678.

The simplest path is Automatic HTTPS with Caddy, which fetches and renews Let's Encrypt certificates for you. Point an A record for your hostname (say n8n.example.com) at the server's public IP, then have Caddy reverse-proxy that hostname to 127.0.0.1:5678. Once DNS resolves, Caddy issues a certificate automatically and n8n is live on HTTPS.

The WEBHOOK_URL gotcha — the #1 self-host mistake. n8n needs to know its own public address to generate the correct webhook and OAuth-callback URLs. That's what N8N_HOST, N8N_PROTOCOL=https, and especially WEBHOOK_URL are for. If these don't match the domain your proxy actually serves, n8n will hand out webhook URLs pointing at localhost:5678 or the wrong scheme — and every inbound webhook and OAuth flow breaks. Set WEBHOOK_URL to your full public origin (https://n8n.example.com/), matching your proxy exactly, and restart the stack after any change:

docker compose up -d

First-run setup

Load https://n8n.example.com in your browser. On a fresh install n8n shows an owner-account setup screen.

Create the owner account immediately. Like most self-hosted tools, the setup screen is open until the first account exists — whoever registers first owns the instance. Set your email and a strong password before you do anything else.

Once you're in, build a first workflow to confirm everything works end to end:

  1. Create a new workflow and add a Manual Trigger (or a Schedule Trigger to verify your timezone is right).
  2. Add a second node — an HTTP Request to a public API is a good smoke test — and connect it to the trigger.
  3. Click Execute Workflow and check the data flows through. Green nodes mean success.
  4. To test webhooks specifically, add a Webhook node and confirm the Production URL it shows starts with your real https://n8n.example.com/ — if it does, your WEBHOOK_URL is correct. Call that URL from outside (e.g. curl from your laptop) and watch the execution fire.

If the webhook URL looks right and an external call triggers a run, your public addressing is correct and you're ready to build real automations.

Backups

Two things must be backed up, and both matter:

  • The Postgres database. This holds your workflows, credentials (encrypted), and execution history — the actual content of your n8n instance.
  • The n8n data volume (/home/node/.n8n). This holds config and internal state, and if you let n8n auto-generate the encryption key, it lives here.

Losing either one hurts. And critically: a Postgres backup is useless without the encryption key. The credentials in the database are encrypted with N8N_ENCRYPTION_KEY — restore the database to a new instance with a different key and every credential is unreadable. So your backup set is really three things: the database dump, the .n8n volume, and the .env file holding the key.

Dump the database on a schedule and copy it, plus the .env, to off-box storage:

docker compose exec -T postgres pg_dump -U n8n n8n | gzip > n8n-db-$(date +%F).sql.gz

A backup that only lives on the same server dies with the server — get these files to an S3-compatible bucket or another machine, and periodically test a restore into a throwaway stack. An untested backup is a guess.

Upgrades

New n8n versions land frequently. To upgrade, pull the newer image and recreate the container:

docker compose pull
docker compose up -d

On start, n8n runs any pending database migrations automatically — which is why a current backup before upgrading is non-negotiable, since a migration changes your data in place.

Two habits make upgrades safe. First, pin a specific image tag rather than riding latest, so you control exactly when you move and can reproduce the running version — change n8nio/n8n:latest to a pinned tag like n8nio/n8n:1.70.0 and bump it deliberately. Second, read the release notes before a major bump. n8n occasionally ships breaking changes to nodes or behavior, and the notes tell you what to check. Back up, read, then upgrade.

Troubleshooting

Webhooks return 404, or a service reports "the URL is not reachable." Almost always a wrong WEBHOOK_URL. If n8n thinks its address is localhost:5678 but the world reaches it at https://n8n.example.com, the URLs it generates point nowhere. Confirm WEBHOOK_URL, N8N_HOST, and N8N_PROTOCOL=https all match your public domain, restart, and re-check the Production URL shown on a Webhook node.

OAuth "redirect URI mismatch." The callback URL n8n generates (derived from your public address) must be registered exactly in the third-party app's OAuth settings — scheme, host, and path all have to match. Copy the OAuth redirect URL from n8n's credential screen and paste it verbatim into the provider; don't hand-type it. A wrong WEBHOOK_URL/N8N_HOST will also produce this, since the callback is built from them.

Saved credentials suddenly fail to decrypt. The N8N_ENCRYPTION_KEY changed or was lost — a different key can't decrypt credentials encrypted with the old one. Restore the original key from your backup. If it's truly gone, the credentials are unrecoverable and must be re-entered. This is why the key belongs in your backups.

Scheduled workflows fire at the wrong time. The container timezone is wrong. Set both GENERIC_TIMEZONE and TZ to your actual timezone and restart.

Container won't start / database connection errors on boot. Usually n8n started before Postgres was ready, or the DB credentials don't match. The depends_on healthcheck in the compose file handles ordering; if you edited it, confirm Postgres is healthy (docker compose ps) and that DB_POSTGRESDB_PASSWORD matches POSTGRES_PASSWORD.

Verification + next steps

You're done when you can: load the n8n editor over HTTPS on your own domain, sign in as the owner account, execute a workflow successfully, trigger a Webhook node from outside the server, and see a database dump plus your .env landing in off-box storage.

From here, connect the integrations your automations need, and prune old execution data periodically so Postgres stays lean. If throughput ever outgrows a single process, look into queue mode with Redis workers — but most instances never need it, which is exactly what makes n8n such a good fit for a small, cheap box. A Hetzner CX22 is the value pick; reach for Kamatera when you want to dial RAM precisely or scale on demand. See Best VPS for self-hosting for the ranked picks.

Next steps

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.