Self-Hosting n8n with Docker Compose: Start on Postgres, Not SQLite

Axel Grubba, October 12, 2026
Start selling digital products with Crevio
Crevio E-Commerce Platforms logo
Crevio
Sponsored
5.0
(1)
Free plan available
Crevio is an AI-powered platform that runs your business while you sleep. Describe what you want to se... Learn more about Crevio
Get an AI summary of this post on:

(New to n8n entirely? What n8n is covers the fair-code licence and what it permits.)

n8n ships with SQLite. It works, and for trying things out n8n says so itself: “SQLite is fine for trying things out, but for a production instance that must handle more than a handful of users or workflows running around the clock use Postgres instead.”

The reason to make that decision on day one rather than later is buried in a single sentence in n8n’s documentation, and it’s the most expensive thing on this page to learn the hard way:

“n8n migrates itself to the new Postgres database on startup. Existing SQLite data doesn’t carry over automatically. This is meant for a fresh instance, not an in-place migration.”

Switch six months in and you don’t get a migration — you get an empty instance and a manual rebuild of every workflow and credential. Ten extra minutes now avoids that entirely.

The stack

Three services: n8n, Postgres for state, and Caddy for automatic TLS.

.env — never commit this:

POSTGRES_USER=change-me-user
POSTGRES_PASSWORD=change-me-password
POSTGRES_DB=n8n

N8N_ENCRYPTION_KEY=generate-a-long-random-string
N8N_HOST=n8n.example.com
WEBHOOK_URL=https://n8n.example.com/
GENERIC_TIMEZONE=Europe/London

compose.yml, following n8n’s documented Postgres configuration:

volumes:
  db-storage:
  n8n-storage:
  caddy-data:

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

  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: always
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: '5432'
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_HOST: ${N8N_HOST}
      WEBHOOK_URL: ${WEBHOOK_URL}
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      N8N_PROTOCOL: https
    volumes:
      - n8n-storage:/home/node/.n8n
    expose:
      - "5678"
    depends_on:
      postgres:
        condition: service_healthy

  caddy:
    image: caddy:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data

Caddyfile — two lines, and TLS is handled:

n8n.example.com {
    reverse_proxy n8n:5678
}

Then:

docker compose up -d

Three details in that file are doing real work.

expose rather than ports on the n8n service. n8n is reachable by Caddy on the internal network and by nothing else. Publishing 5678 would put it on every interface — and ufw wouldn’t stop it, because Docker routes published ports before ufw’s chains. Only Caddy binds to the host.

depends_on with condition: service_healthy. Without the healthcheck condition, n8n starts before Postgres is accepting connections and dies. This is the most common “it won’t start” report for this stack.

WEBHOOK_URL. n8n builds the webhook URLs it hands to third-party services from this. Get it wrong and workflows save fine, then never receive anything — a failure with no error attached.

Set the encryption key deliberately

N8N_ENCRYPTION_KEY encrypts stored credentials. If you don’t set it, n8n generates one into the .n8n directory — which is fine right up until you rebuild the container without that volume, at which point every stored credential is undecryptable.

Set it explicitly, put it in your password manager, and keep the .n8n volume mounted. n8n advises persisting that directory even on Postgres, because it also holds “encryption keys, instance logs, and source control feature assets.”

Stop it filling the disk

n8n saves execution data by default, and on a busy instance that grows quickly. The relevant defaults, from n8n’s configuration reference:

Variable Default What it does
EXECUTIONS_DATA_PRUNE true Deletes old execution data on a rolling basis
EXECUTIONS_DATA_MAX_AGE 336 Age in hours before deletion — 14 days
EXECUTIONS_DATA_PRUNE_MAX_COUNT 10000 Max executions retained; 0 = no limit

Pruning is on by default, which is better than most self-hosted software manages. But 14 days and 10,000 executions is generous for a high-frequency instance on a small disk. If you’re running thousands of executions a day, lower both.

Two more worth knowing: EXECUTIONS_DATA_SAVE_ON_SUCCESS defaults to all, so every successful run is stored. Setting it to none keeps failures only — usually what you actually want, and a large saving.

Queue mode, and when you need it

By default EXECUTIONS_MODE is regular — the main process runs the workflows. Queue mode adds Redis and separate worker containers, and n8n describes it as providing “the best scalability.”

The licensing, precisely, because it’s easy to get wrong: single-main queue mode is available on self-hosted — EXECUTIONS_MODE=queue with Redis and workers has no licence gate. What does require an Enterprise licence is multi-main setup (N8N_MULTI_MAIN_SETUP_ENABLED), which is several main instances behind a load balancer. Queue mode on n8n Cloud is also an Enterprise plan feature, but that’s a Cloud constraint, not a self-hosted one.

You need queue mode when a long-running workflow blocks others, when concurrent webhook bursts are being dropped, or when a single process can’t keep up. You do not need it because it sounds more professional — it adds Redis, worker containers and a queue to debug, and most single-purpose instances never justify it.

Sizing

Workload Spec
Light — a few workflows, modest volume 2 vCPU / 4GB
Community nodes, larger payloads, browser automation 4 vCPU / 8GB
Queue mode with workers 8GB+, and split services if you can

Memory is what kills n8n, not CPU. Large payloads, big loops and anything holding a dataset in memory are the culprits — we measured three concurrent 10MB payloads costing more than two hundred trivial executions, and the failure is abrupt — the kernel kills the process and n8n’s own log shows nothing. If your instance dies mid-workflow with no error, that’s an OOM kill, not a bug.

That’s also the argument for self-hosting over n8n Cloud: the Cloud Starter plan provides 320 MiB of RAM, and a 4GB VPS matches its Enterprise tier. We covered the whole comparison in migrating n8n Cloud to self-hosted.

Where to run it

Hostinger has a one-click n8n VPS template, which skips this entire compose file if you’d rather not maintain one — and its 8GB tier is the bracket most instances land in. Hetzner is the cheapest per gigabyte when its CX tier is in stock — worth checking before you commit, because that tier was showing as unavailable when we last looked, and the CPX line you’d fall back to is several times dearer. A plain Docker host is exactly its strength. Cloudways is the managed option if you want the resources without administering the box, and UltaHost is worth pricing at the budget end — confirm the plan specs in writing, as we’ve found its published figures harder to pin down than its marketing implies.

DigitalOcean is a common choice for this stack as well, though its tracked link isn’t currently working on our end, so we’ve left it unlinked.

Our best VPS for n8n guide covers sizing in more depth.

How we checked this

The Postgres service definition, healthcheck, DB_* environment variables and depends_on condition are from n8n’s own Docker Compose documentation, read in August 2026, as are the execution-pruning defaults (EXECUTIONS_DATA_PRUNE true, EXECUTIONS_DATA_MAX_AGE 336 hours, EXECUTIONS_DATA_PRUNE_MAX_COUNT 10000), the EXECUTIONS_MODE default of regular, and the guidance to keep persisting the .n8n directory on Postgres.

One claim we did not repeat. The brief for this article asserted that SQLite “silently corrupts under concurrent webhook load.” n8n’s documentation makes no such claim — it says SQLite is fine for trying things out and recommends Postgres for production instances handling more than a handful of users or round-the-clock workflows. We found nothing to substantiate silent corruption, so we’ve led with the reason that is documented and arguably more compelling: switching later doesn’t migrate your data.

On queue-mode licensing, n8n’s docs mention Enterprise licences in two nearby contexts — Cloud queue mode, and multi-instance/multi-main setups. Neither gates single-main queue mode on self-hosted, and we’ve said so precisely rather than repeating a blanket “queue mode needs Enterprise” that would be wrong.

What we did not do: we did not deploy this stack for the article. The compose file is assembled from n8n’s documented configuration rather than a running deployment, so validate it against your own version — docker compose logs n8n on first start will tell you more than we can. We also haven’t load-tested the sizing figures; they’re conventional brackets, and your payload sizes move them.

The hosting links above are affiliate links. Every configuration decision in this article is free.

FAQ

Should I use SQLite or Postgres for self-hosted n8n?

Postgres, from the start. n8n says SQLite is fine for trying things out, but switching later doesn’t bring your data — its docs describe the Postgres switch as “meant for a fresh instance, not an in-place migration,” so a late change means rebuilding workflows and credentials by hand.

Why won’t n8n start with my Docker Compose file?

Most often it started before Postgres was ready. Use depends_on with condition: service_healthy against a pg_isready healthcheck, which is what n8n’s own documented compose file does.

Do I need Redis and queue mode?

Only if a long-running workflow blocks others or concurrent webhooks are being dropped. EXECUTIONS_MODE defaults to regular and that’s adequate for most single-purpose instances. Queue mode adds Redis and worker containers to operate and debug.

Does queue mode need an Enterprise licence?

Not for single-main self-hosted — set EXECUTIONS_MODE=queue with Redis and workers. Multi-main setup does require a licence, and queue mode on n8n Cloud is an Enterprise plan feature. Those are different things and often conflated.

How do I stop n8n filling my disk?

Pruning is already on: 14 days (EXECUTIONS_DATA_MAX_AGE=336) and 10,000 executions by default. On a busy instance lower both, and consider EXECUTIONS_DATA_SAVE_ON_SUCCESS=none so only failures are retained.

How much RAM does self-hosted n8n need?

4GB for light use, 8GB once you add community nodes, large payloads or browser automation. Memory is the binding constraint rather than CPU, and running out kills the process with no error in n8n’s log.

Why do my webhooks never fire?

Usually WEBHOOK_URL is wrong or unset, so n8n hands third-party services a URL that doesn’t reach it. The workflow saves and activates normally, which is what makes it hard to spot.

Founder & Software Review Editor
Axel Grubba is the founder of Findstack, a B2B software comparison platform, with his background spanning management consulting and venture capital where he invested in software. Recently, Axel has developed a passion for coding and enjoys traveling when he is not building and improving Findstack.
Business Software Reviews SaaS Product Evaluation CRM Software
Subscribe, get software deals straight to your inbox.
Join 8,300+ other entrepreneurs staying up-to-date on all the latest deals.
Zero spam. Unsubscribe at any time.