Guide

How to self-host n8n on a VPS

This guide sets up n8n on a Linux VPS with Docker Compose, Postgres and HTTPS, then covers the decisions that come later: when to switch on queue mode, what to back up and how to update without losing credentials. Version numbers, variable names and limits are taken from the n8n documentation as it reads in November 2026. Check the linked pages before you rely on a detail, because n8n changes quickly.

By Evorxa teamPublished

What the license allows

Self-hosted n8n is published under the Sustainable Use License, a fair-code license n8n wrote in 2022. It lets you use, modify and redistribute the software for free, with three limits: you may use it only for your own internal business purposes or for non-commercial and personal use; you may give it to others only free of charge and for non-commercial purposes; and you may not remove n8n's licensing or copyright notices.

The line that settles most cases is the first one in n8n's license FAQ: if workflows are only created or modified by you or people in your organization, you can use the Community license for free.

Allowed under the Community license

  • Running your own business on it, and personal projects, learning or research.
  • Building automations for clients on your instance, as long as the clients cannot create or edit them.
  • Charging clients for building, setting up and maintaining workflows, and for training and consulting.
  • Using n8n behind the scenes in your own product, where users trigger automations you built and even connect their own accounts.
  • Running several instances.

Not allowed without a commercial agreement

  • Hosting n8n as a service where your clients build workflows.
  • Letting outside users build or configure their own workflows through your product, whether through your own interface, the API, MCP or an AI agent acting for them.
  • Forking the code to launch your own automation product, or white-labelling n8n by removing its branding.
  • Using enterprise features that ask for a license key without an Enterprise license.

If your plan sits between the two lists, n8n asks you to write to license@n8n.io before you build it.

How big the server needs to be

n8n is light on CPU. Its prerequisites page says memory requirements usually matter more than CPU, and that an idle n8n Cloud instance uses about 100 MB. What raises memory is the work itself: large JSON payloads, binary files such as PDFs and images, many nodes in one workflow, the Code node, and manual test runs, which keep an extra copy of the data for the editor.

Sizes the n8n documentation states
SetupDocumented figureWhere it comes from
n8n at idleAbout 100 MB RAMPrerequisites page, n8n Cloud at idle
n8n process, typical range320 MB to 2 GB RAMPrerequisites page
Database storage512 MB to 4 GB SSDPrerequisites page
Full Compose stack with the n8n Assistant sandboxAt least 4 GB RAM and 2 vCPUsDocker Compose install guide

Add Postgres, the reverse proxy and the operating system on top of the n8n figure. The number to plan for is the peak during your heaviest workflow, not the idle figure: run that workflow once on the new server and watch docker stats while it executes. If you plan to run the n8n Assistant sandbox from the official Compose file, treat 4 GB and 2 vCPUs as the floor.

Docker Compose with Postgres

Out of the box n8n stores everything in SQLite. The Docker Compose guide says SQLite is fine for trying things out, but a production instance with more than a handful of users, or workflows running around the clock, should use Postgres. Queue mode does not support SQLite at all, so starting on Postgres saves a migration later.

Install Docker Engine with the Compose plugin, create a folder such as ~/n8n, and put two files in it. First the .env file. Pin N8N_VERSION to a real release number from n8n's GitHub releases page, and generate the encryption key yourself so it is written down somewhere other than the server:

.env
# .env (keep it out of version control)
N8N_VERSION=2.xx.x
N8N_HOST=n8n.example.com
N8N_ENCRYPTION_KEY=change-me-long-random-string
POSTGRES_USER=n8n
POSTGRES_PASSWORD=change-me-password
POSTGRES_DB=n8n
GENERIC_TIMEZONE=Europe/Amsterdam

Then compose.yaml:

compose.yaml
services:
  postgres:
    image: postgres:18
    restart: always
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      PGDATA: /var/lib/postgresql/data
    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: n8nio/n8n:${N8N_VERSION}
    restart: always
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      N8N_HOST: ${N8N_HOST}
      N8N_PORT: 5678
      N8N_PROTOCOL: https
      NODE_ENV: production
      N8N_WEBHOOK_URL: https://${N8N_HOST}/
      N8N_PROXY_HOPS: 1
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${GENERIC_TIMEZONE}
      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}
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  db-storage:
  n8n_data:
  • Keep the PGDATA line. Postgres 18 changed its default data location, and without that line the database is written outside the volume and starts empty after a recreate.
  • Port 5678 is bound to 127.0.0.1, so only the reverse proxy on the same server can reach n8n. Nothing but 80 and 443 needs to be open to the internet.
  • The n8n_data volume at /home/node/.n8n still matters with Postgres: it holds the encryption key file, logs and source control assets.

Start it with docker compose up -d. n8n creates its tables in Postgres on first start. Data from an earlier SQLite instance does not move across by itself; export workflows and credentials with the CLI commands in the backup section and import them.

HTTPS, the reverse proxy and webhook URLs

n8n builds webhook URLs from N8N_PROTOCOL, N8N_HOST and N8N_PORT. Behind a proxy that gives the wrong answer, because n8n listens on 5678 while the public URL is on 443. The reverse proxy page asks for three things:

  1. Set N8N_WEBHOOK_URL to the public address, for example https://n8n.example.com/. It replaced WEBHOOK_URL, which is deprecated from n8n 2.35.0. On versions before 2.30.0, use WEBHOOK_URL.
  2. Set N8N_PROXY_HOPS=1 when one proxy sits in front of n8n.
  3. Make the last proxy pass X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto.

Any proxy works if it does those three. With Caddy, the whole configuration is four lines. Caddy's reverse_proxy directive sets all three forwarded headers and passes WebSocket connections without extra settings, and Caddy requests the certificate for the domain itself once DNS points at the server:

Caddyfile
# /etc/caddy/Caddyfile
n8n.example.com {
    reverse_proxy 127.0.0.1:5678
}

If webhooks show localhost:5678 in the editor, or external services reject the URL, N8N_WEBHOOK_URL is missing or the container was not recreated after you set it. Run docker compose up -d again after any change to the environment.

Queue mode: when you need Redis and workers

In the default mode one n8n process runs the editor, receives webhooks and executes every workflow. That is enough for most single-team instances. Two signs that it is no longer enough: executions pile up behind long-running ones, or the editor stops responding while heavy workflows run.

Try the cheap fix first. N8N_CONCURRENCY_PRODUCTION_LIMIT caps how many production executions run at once and queues the rest; it is off by default. If one process is still the bottleneck, queue mode splits the work: the main process receives triggers and webhooks, Redis holds the queue, and worker processes run the executions.

compose.yaml additions
# 1. in the n8n service: name the environment block, add the queue settings
    environment: &n8n-env
      # ...everything from before, plus:
      EXECUTIONS_MODE: queue
      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

# 2. two new services
  redis:
    image: redis:7
    restart: always

  n8n-worker:
    image: n8nio/n8n:${N8N_VERSION}
    restart: always
    command: worker --concurrency=10
    environment: *n8n-env
    depends_on:
      - postgres
      - redis
  • Every process needs the same N8N_ENCRYPTION_KEY, the same database settings and the same image version.
  • A worker runs 10 jobs at once by default. n8n recommends 5 or more per worker: many workers with low concurrency can exhaust the database connection pool.
  • Queue mode with SQLite is not supported, and binary data cannot stay on the local filesystem; n8n points to S3 external storage for that.
  • For heavy webhook traffic, add webhook processors (n8n webhook) and set N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true on the main process. Running several main processes needs an Enterprise license.

Backups that can restore credentials

A full backup has two parts, according to n8n's backup and restore page: the .n8n folder, which holds the encryption key, and the Postgres database. Credentials in the database are encrypted with that key, so a database dump without the key cannot restore them.

shell
# workflows and credentials as JSON, one file each
docker compose exec n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/workflows/
docker compose exec n8n n8n export:credentials --backup --output=/home/node/.n8n/backups/credentials/

# the whole Postgres database
docker compose exec -T postgres pg_dumpall -U n8n > n8n-db-$(date +%F).sql
  • The --backup flag writes each workflow and credential as its own JSON file. Use separate folders for workflows and credentials; importing credentials fails when workflow files share the folder.
  • CLI exports leave out users, execution history, variables and instance settings, including the key. They move workflows between instances; they do not rebuild one.
  • Copy the dump, the exports and the .n8n volume off the server. A backup on the same disk does not survive losing the server.
  • Imported workflows arrive deactivated. Plan to switch them back on after a restore.

Updating without surprises

n8n's update page suggests updating at least once a month, so you never jump many versions at once, and reading the release notes for every version between yours and the target.

shell
# 1. back up first (see above), then set the new N8N_VERSION in .env
docker compose pull
docker compose up -d
docker compose logs -f n8n

Take the full backup first. Each process runs pending database migrations when it starts, and setting the image tag back to the old version does not reverse them. Rolling back means running n8n db:revert one migration at a time with the newer image, or restoring the backup. In queue mode, update the main process, workers and webhook processors to the same tag together.

Common failures and fixes

Symptoms and their usual cause
What you seeLikely causeWhat to do
"Execution stopped at this node (n8n may have run out of memory while executing it)", "Connection Lost" or 503 errorsThe process ran out of memory and restartedSplit data into batches (200 rows instead of 10,000), move heavy steps into sub-workflows, avoid the Code node and manual runs for large data
"Allocation failed - JavaScript heap out of memory" in the logsNode.js heap limit reachedRaise it with NODE_OPTIONS (below) if the server has free RAM, or add RAM
Webhook URLs show localhost or the wrong portN8N_WEBHOOK_URL not set or container not recreatedSet it to the public HTTPS URL and run docker compose up -d
Credentials fail to decrypt after a moveDifferent encryption keyRestore the original .n8n folder or set the original N8N_ENCRYPTION_KEY
Postgres exits with "database files are incompatible with server"Image moved to a new major version on an old data folderGo back to the old tag, pg_dumpall, then follow the PostgreSQL upgrade guide
Disk keeps filling upExecution historyPruning is on by default (14 days or 10,000 executions); lower EXECUTIONS_DATA_MAX_AGE or EXECUTIONS_DATA_PRUNE_MAX_COUNT

The memory fixes come from n8n's page on memory-related errors. Raising the heap limit looks like this:

compose.yaml
# n8n service environment: raise the Node.js heap limit (value in MB)
      NODE_OPTIONS: --max-old-space-size=2048

Running this on Evorxa

Everything above works on an Evorxa server, with root and SSH access. You can start from a plain Ubuntu or Debian image, usually ready in about a minute, or pick the one-click n8n app, which takes a few minutes more to install. Plans start at $2.99 a month and are prepaid monthly, for six months or yearly. Servers run in one region, Eygelshoven in the Netherlands.

Sources

The facts in this guide come from these pages.

  1. n8n: Sustainable Use License and license FAQ
  2. n8n: Install using Docker Compose (Postgres section)
  3. n8n: Configure webhook URLs with a reverse proxy
  4. n8n: Enable queue mode
  5. n8n: Back up and restore
  6. n8n: Memory-related errors