To self-host n8n, install Docker on a VPS and run the official n8n image together with PostgreSQL in Docker Compose. Publish n8n’s port 5678 on 127.0.0.1 only and put Nginx with a free Let’s Encrypt certificate in front of it. Three settings make it work behind the proxy: N8N_HOST, N8N_PROTOCOL=https and N8N_WEBHOOK_URL with your public address. Then back up two things, the database and the encryption key. Without the key, the credentials stored in n8n cannot be decrypted.
# 1. Docker Engine with the Compose plugin
docker compose version
# 2. a project folder with .env and compose.yaml (both below)
sudo mkdir -p /opt/n8n && cd /opt/n8n
openssl rand -hex 32 # becomes N8N_ENCRYPTION_KEY in .env
docker compose up -d
# 3. Nginx in front of 127.0.0.1:5678, then HTTPS
sudo certbot --nginx -d n8n.example.comThe steps below go through each file, then cover backups, updates and the security settings that matter once the editor is on the internet.
What do I need to self-host n8n?
- A VPS with root access running Debian 12 or 13 or Ubuntu 22.04 or 24.04, with Docker Engine and the Compose plugin. If Docker is not installed yet, install Docker on Ubuntu or Debian first. n8n 3.0 ends support for installs via npm, so Docker is the way to self-host it from here on.
- A domain or subdomain such as
n8n.example.comwith an A record pointing at the server. Webhooks and OAuth logins need a stable HTTPS address, and Let’s Encrypt only issues a certificate once the name resolves. See pointing a domain at your VPS. - Ports 80 and 443 open in the firewall for Nginx. Port 5678 stays closed.
- Memory sized for your workflows rather than for n8n itself. An idle n8n uses little. Memory grows with the amount of data each execution handles, binary files, Code nodes, parallel runs and manual test runs, which keep a copy of the data for the editor. PostgreSQL needs its share too. n8n’s own Compose guide asks for at least 4 GB of RAM and 2 vCPUs for its full stack including the AI assistant’s sandbox, and its documentation on memory errors explains the factors in detail.
Should I use SQLite or PostgreSQL?
Without any database settings, n8n stores everything in a SQLite file inside its data folder. That is fine for trying n8n out or for a few personal workflows. For an instance you will keep, n8n recommends PostgreSQL, and you need it for queue mode with separate workers. MySQL and MariaDB are no longer supported as n8n’s database since n8n 2.0.
Decide at the start. Switching later does not move your data automatically: you export workflows and credentials from the old instance and import them into the new one. The setup below uses PostgreSQL; n8n’s database documentation lists the PostgreSQL major versions it supports, so check it before you change the image tag.
How do I set up n8n with Docker Compose?
- 1Create the .env file
All secrets and names live in
/opt/n8n/.env. Generate the encryption key and the database password withopenssl rand -hex 32and paste them in./opt/n8n/.env # n8n image tag: "stable" is the newest stable release. # Pin an exact version from github.com/n8n-io/n8n/releases to decide when you update. N8N_VERSION=stable DOMAIN=n8n.example.com GENERIC_TIMEZONE=Europe/Amsterdam POSTGRES_USER=n8n POSTGRES_PASSWORD=paste-a-long-random-password-here POSTGRES_DB=n8n # encrypts every credential stored in n8n: back this file up N8N_ENCRYPTION_KEY=paste-the-output-of-openssl-rand-hex-32-herelock it down sudo chmod 600 /opt/n8n/.env - 2Create compose.yaml
/opt/n8n/compose.yaml services: postgres: image: postgres:18 restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} # Postgres 18 moved its default data path; keep it where the volume is mounted PGDATA: /var/lib/postgresql/data volumes: - db_data:/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:${N8N_VERSION} restart: unless-stopped ports: - "127.0.0.1:5678:5678" # reachable from this server only, i.e. from Nginx 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: ${DOMAIN} N8N_PORT: "5678" N8N_PROTOCOL: https N8N_WEBHOOK_URL: https://${DOMAIN}/ # called WEBHOOK_URL before n8n 2.35 N8N_PROXY_HOPS: "1" # trust X-Forwarded-* from one proxy (Nginx) GENERIC_TIMEZONE: ${GENERIC_TIMEZONE} # schedule triggers TZ: ${GENERIC_TIMEZONE} # system time inside the container NODE_ENV: production volumes: - n8n_data:/home/node/.n8n # settings, encryption key, logs depends_on: postgres: condition: service_healthy volumes: db_data: n8n_data:Named volumes avoid permission trouble: the n8n container runs as the user
node(UID 1000), and a bind-mounted host folder would first needsudo chown -R 1000:1000. - 3Start it and check the logs
/opt/n8n docker compose up -d docker compose ps # both services "running", postgres "healthy" docker compose logs -f n8n # wait for "Editor is now accessible via" curl -sf http://127.0.0.1:5678/healthz # {"status":"ok"}
If docker answers “permission denied” on the socket, run the commands with sudo or add your user to the docker group; the Docker socket guide explains both. If docker compose itself is missing, see docker: command not found.
How do I put n8n behind Nginx with HTTPS?
Nginx terminates TLS and passes requests to 127.0.0.1:5678. Two details are specific to n8n: the editor keeps a WebSocket connection open, so the Upgrade headers must pass through, and webhook calls that wait for a workflow to finish can take longer than Nginx’s default 60-second read timeout.
server {
listen 80;
server_name n8n.example.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
# the editor's live connection is a WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; # stream responses as they come
proxy_read_timeout 300s; # webhooks that wait for the workflow
client_max_body_size 16m; # matches n8n's default payload limit
}
}sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d n8n.example.comCertbot adds the HTTPS server block and the redirect from HTTP, and renews the certificate on its own. The Nginx reverse proxy guide explains every line in more depth. If you raise client_max_body_size for large uploads, raise N8N_PAYLOAD_SIZE_MAX (in MiB) to match.
Now open https://n8n.example.com and create the owner account straight away. Until you do, the setup page is open to anyone who finds the address.
Why do my webhook URLs show localhost:5678?
n8n builds webhook URLs from N8N_PROTOCOL, N8N_HOST and N8N_PORT. Inside a container without those settings that gives http://localhost:5678/webhook/…, an address no outside service can reach. Behind a reverse proxy, set the public address explicitly:
N8N_WEBHOOK_URL: https://n8n.example.com/
N8N_PROXY_HOPS: "1"- The variable’s name changed. Since n8n 2.35 it is
N8N_WEBHOOK_URL. The oldWEBHOOK_URLstill works with a deprecation warning, and it is the only name older versions understand, which is why most tutorials use it. - Recreate the container after changing it:
docker compose up -dapplies new environment values,docker compose restartdoes not. - Re-register existing triggers. Services such as Telegram or Stripe stored the old URL when the workflow went live. Unpublish the workflow and publish it again so the trigger registers the new one.
- Test and production URLs differ.
/webhook-test/…only answers while the editor is listening for a test event;/webhook/…only answers while the workflow is published.
A related message, “Your n8n server is configured to use a secure cookie”, appears when you open the editor over plain http://, for example at http://SERVER-IP:5678. Use the HTTPS domain. N8N_SECURE_COOKIE=false exists for test setups on a private network, not for a server on the internet.
What must I back up?
Two things, and they only work together: the PostgreSQL database, which holds workflows, credentials, users and execution history, and the encryption key, which decrypts the credentials inside it. A database restored without its key gives you every workflow and not one working login.
- The key: with the setup above it is in
/opt/n8n/.env. If you started withoutN8N_ENCRYPTION_KEY, n8n generated one and saved it in the data volume:docker compose exec n8n cat /home/node/.n8n/configshows it. - The database: a
pg_dumpon a schedule, as in the script below. - Optional, portable copies:
n8n export:workflow --backupandn8n export:credentials --backupwrite one JSON file per workflow and credential. They are handy for moving work between instances but contain no users, execution history or settings.
#!/bin/sh
set -e
cd /opt/n8n
mkdir -p /var/backups/n8n
chmod 700 /var/backups/n8n
day=$(date +%F)
# keep two weeks
find /var/backups/n8n -type f -mtime +14 -delete
# database, in pg_dump's compressed custom format
docker compose exec -T postgres pg_dump -U n8n -d n8n -Fc > /var/backups/n8n/n8n-$day.dump
# the key and settings that belong to it
cp .env /var/backups/n8n/env-$day
# portable JSON copies, written into the persistent data volume
docker compose exec -T n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/workflows/ \
|| echo "workflow export failed (an instance without workflows has nothing to export)"30 3 * * * /usr/local/bin/n8n-backup >> /var/log/n8n-backup.log 2>&1Make the script executable with sudo chmod +x /usr/local/bin/n8n-backup and run it once by hand. If the nightly run never seems to happen, work through cron job not running. Copy the backups to a second machine as well: a backup on the same disk does not survive the disk.
A restore replaces the current database and .env with the backup: everything changed since that night is gone. If the current state might still matter, run the backup script once more before you restore.
cd /opt/n8n
cp /var/backups/n8n/env-2026-11-14 .env # the key that belongs to this dump
docker compose stop n8n
docker compose exec -T postgres pg_restore -U n8n -d n8n --clean --if-exists < /var/backups/n8n/n8n-2026-11-14.dump
docker compose up -dHow do I update n8n safely?
/usr/local/bin/n8n-backup # 1. a fresh backup
# 2. read the release notes; set N8N_VERSION in .env if you pin versions
docker compose pull # 3. fetch the new image
docker compose up -d # 4. recreate the container
docker compose logs -f n8n # 5. watch the database migrations finishOn startup a new version migrates the database, and there is no way back except restoring the backup and the old image tag. That is the reason to pin a version and update on purpose rather than pulling stable blindly. Before a major upgrade, such as 2.x to 3.0, read n8n’s breaking-changes page and open Settings → Migration Report, which lists the workflows and settings the new version affects.
Why does the n8n database keep growing?
Execution history is the part that grows: every run stores its input and output data. By default n8n deletes executions older than 14 days (EXECUTIONS_DATA_MAX_AGE=336, in hours) and keeps at most 10,000 (EXECUTIONS_DATA_PRUNE_MAX_COUNT). Lower both in the n8n environment if the database grows faster than your disk, and see checking disk usage on Linux for old Docker images and logs, the other usual suspects.
How do I keep a self-hosted n8n secure?
- Never publish 5678 on all interfaces. Docker writes its own firewall rules for published ports, and they take effect before UFW’s, so
"5678:5678"is reachable from the internet even with UFW set to deny. Binding to127.0.0.1avoids that. - Turn on two-factor authentication for the owner account in your personal settings. Old guides that use
N8N_BASIC_AUTH_*variables are out of date: n8n removed basic auth in 1.0 in favour of its own user accounts. - Protect your webhooks. A production webhook URL accepts requests from anyone. Set Header Auth, Basic Auth or JWT in the Webhook node’s authentication option, or check a shared secret first thing in the workflow.
- Harden SSH and keep the server updated. Key-only logins and fail2ban take ten minutes: see securing SSH.
- Update n8n regularly. Its releases include security fixes, and the editor is a web application holding the keys to every service you connect.
When do I need queue mode?
A single n8n container runs the editor, the triggers and every execution in one process. That carries a lot of workflows, but a burst of heavy executions slows the editor down with it. Queue mode splits the work: the main instance receives webhooks and triggers and puts executions into a Redis queue, and one or more worker containers started with the command worker take them from there. All of them share PostgreSQL (queue mode on SQLite is not supported), set EXECUTIONS_MODE=queue and QUEUE_BULL_REDIS_HOST, and must use the same N8N_ENCRYPTION_KEY. Start with one container and move to queue mode when executions, not the editor, are what keeps the CPU busy.
Is self-hosted n8n free, and what does the licence allow?
The self-hosted Community Edition costs nothing; you pay for the server. n8n is “fair-code” under its Sustainable Use License, not an open-source licence in the OSI sense. In n8n’s own summary, you may use it for free for your own internal business purposes and for personal or non-commercial use, and consultants may build workflows for clients who receive the results. Hosting n8n as a service in which your customers build their own workflows, or white-labelling it, is not allowed. Some features, such as single sign-on, source control and external secrets, need a paid licence. Read the licence and n8n’s licence FAQ before you build a business on it; this is a summary, not legal advice.
Which VPS should I run n8n on?
A Standard KVM 1 plan with 2 cores, 4 GB of RAM and 20 GB of storage, from 4.99 EUR a month in Amsterdam, is a sensible start for n8n with PostgreSQL. If your workflows spend their time in Code nodes rather than waiting on APIs, Hi-CPU KVM on AMD EPYC suits that better; take a plan with at least 4 GB of RAM. Either way your workflows, credentials and execution data stay on your own server in the EU, and VMHeaven also takes payment in crypto.
Frequently asked
How much RAM does n8n need?
There is no fixed figure: an idle n8n uses little, and memory grows with the data each execution handles, binary files, Code nodes, parallel runs and manual test runs. PostgreSQL needs its own share. n8n's Compose guide asks for at least 4 GB of RAM and 2 vCPUs for its full stack, and a 4 GB VPS is a sensible start for n8n with PostgreSQL. Watch 'docker stats' and move to more memory if the logs show 'JavaScript heap out of memory'.
Should I use SQLite or PostgreSQL for n8n?
SQLite is n8n's default and is fine for testing or a few personal workflows. For an instance you will keep, use PostgreSQL: n8n recommends it for production and requires it for queue mode with separate workers. Data does not move between them automatically, so choose before you build workflows. MySQL and MariaDB are no longer supported as n8n's database since n8n 2.0.
Is self-hosted n8n free?
The Community Edition is free to self-host; you pay only for the server. It is fair-code under n8n's Sustainable Use License, which allows internal business and personal use but not offering n8n as a hosted service where customers build their own workflows. Some features, such as single sign-on, source control and external secrets, need a paid licence.
Why do my n8n webhook URLs show localhost:5678?
n8n builds webhook URLs from N8N_PROTOCOL, N8N_HOST and N8N_PORT, which inside a container default to http, localhost and 5678. Behind a reverse proxy, set N8N_WEBHOOK_URL=https://your-domain/ (WEBHOOK_URL on versions before 2.35) and N8N_PROXY_HOPS=1, recreate the container with 'docker compose up -d', then unpublish and publish workflows so their triggers register the new URL.