n8n is a workflow automation tool, similar to Zapier or Make, that you can run on your own server. Self-hosting keeps your data and credentials under your control and has no per-task pricing, which makes it popular with sysadmins and small teams. This guide sets up a production-ready instance on a Linux VPS: Docker Compose, a PostgreSQL database, HTTPS on your own domain, backups and updates.

Last updated: 23 October 2026. Written for n8n 2.x (latest stable at the time of writing: 2.44.6) with Docker Compose, PostgreSQL 18 and Caddy for automatic HTTPS. The Compose file was validated with docker compose config. Always check the n8n releases page for the current version.
What you need
- A Linux server (Ubuntu 24.04 or Debian 12/13 recommended). For a personal or small-team instance, 2 vCPU and 4 GB RAM is a comfortable starting point.
- A domain or subdomain, for example
n8n.example.com, with a DNS A record pointing to the server’s public IP. - Ports 80 and 443 open to the internet (Caddy needs them to get a Let’s Encrypt certificate). Keep SSH restricted; see our server security guide.
- Docker Engine with the Compose plugin, installed from Docker’s official repository following the Docker installation guide. Distribution packages are often outdated.
# Check Docker and Compose are installed
docker --version
docker compose versionStep 1: Create the project folder and secrets
mkdir -p ~/n8n && cd ~/n8n
# Generate the secrets and write the .env file
cat > .env <<EOF
N8N_DOMAIN=n8n.example.com
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .envReplace n8n.example.com with your domain. Back up the .env file somewhere safe. n8n uses N8N_ENCRYPTION_KEY to encrypt every saved credential; if you lose it, you cannot decrypt your credentials after a restore.
Step 2: Write the Compose file
Create compose.yaml in the same folder:
services:
postgres:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- db_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:2.44.6
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
N8N_HOST: ${N8N_DOMAIN}
N8N_PORT: "5678"
N8N_PROTOCOL: https
N8N_PROXY_HOPS: "1"
N8N_WEBHOOK_URL: https://${N8N_DOMAIN}/
N8N_EDITOR_BASE_URL: https://${N8N_DOMAIN}/
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
GENERIC_TIMEZONE: Asia/Kolkata
TZ: Asia/Kolkata
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: "5432"
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "336"
volumes:
- n8n_data:/home/node/.n8n
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
volumes:
db_data:
n8n_data:
caddy_data:
caddy_config:What the important settings do:
| Setting | Purpose |
|---|---|
N8N_HOST, N8N_PROTOCOL | The public host name and protocol of your instance |
N8N_WEBHOOK_URL | Public URL used in webhook nodes. It replaces the older WEBHOOK_URL, which is deprecated since n8n 2.35 |
N8N_PROXY_HOPS: "1" | Tells n8n it runs behind one reverse proxy (Caddy), so it sees the real client IP |
N8N_ENCRYPTION_KEY | Encrypts stored credentials; must stay the same forever |
GENERIC_TIMEZONE, TZ | Time zone for Schedule triggers and system time |
EXECUTIONS_DATA_PRUNE, EXECUTIONS_DATA_MAX_AGE | Delete execution history older than 336 hours (14 days) so the database does not grow forever |
No ports on n8n | n8n is only reachable through Caddy, never directly on port 5678 |
The image tag is pinned to a specific version so updates happen only when you choose. The PostgreSQL 18 image stores data under /var/lib/postgresql, which is why the volume is mounted there.
Step 3: Configure HTTPS with Caddy
Create a file called Caddyfile (replace the domain). Caddy obtains and renews a Let’s Encrypt certificate automatically.
n8n.example.com {
reverse_proxy n8n:5678
}Step 4: Start and verify
docker compose config --quiet && echo "Compose file OK"
docker compose up -d
docker compose ps
docker compose logs -f n8n # Ctrl+C to stop following
# From any machine: should return {"status":"ok"}
curl -s https://n8n.example.com/healthzOpen https://n8n.example.com and create the owner account straight away; until you do, anyone who finds the URL can claim the instance. Then turn on two-factor authentication for your user in n8n’s personal settings.
Step 5: Build a first workflow
A useful first workflow for sysadmins is a simple website uptime check:
- Schedule Trigger: run every 5 minutes.
- HTTP Request: GET your website URL. Under options, enable “Never Error” and “Include Response Headers and Status” so a failed request is passed on instead of stopping the workflow.
- If: check whether the status code is not equal to 200.
- Send Email, Telegram or Slack: on the “true” branch, send an alert with the status code and time.
Test it with the “Execute workflow” button, then activate it. Other common uses include sending server alerts to chat, collecting form submissions into a spreadsheet, and creating tickets from emails. Store API keys in n8n Credentials, never directly in node fields or Code nodes.
Step 6: Back up
cd ~/n8n
mkdir -p backups
# Database dump (workflows, credentials, users, executions)
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/n8n-db-$(date +%F).sql.gz
# n8n data volume (encryption key file, settings)
docker run --rm -v n8n_n8n_data:/data -v "$PWD/backups":/backup alpine \
tar czf /backup/n8n-data-$(date +%F).tar.gz -C /data .The volume name is the project folder name plus the volume name (n8n_n8n_data here); check it with docker volume ls. Copy backups and the .env file off the server, and test a restore on a spare machine.
Step 7: Update safely
n8n releases often and has fixed several critical security vulnerabilities, so keep it current. Read the release notes, take a backup, then change the image tag in compose.yaml to the new version and run:
docker compose pull
docker compose up -d
docker compose logs --tail 50 n8n
docker image prune -fWatch the n8n security advisories and update the PostgreSQL and Caddy images regularly too.
Security checklist for self-hosted n8n
- Create the owner account immediately and enable 2FA for every user.
- Expose only ports 80/443 (Caddy). Remember that ports published by Docker bypass ufw rules, so do not add a
portsentry to the n8n or postgres services. - Keep n8n updated and pin versions.
- Use separate, least-privilege API keys in credentials and rotate them if a workflow or account is compromised.
- Protect public webhook URLs with header or basic authentication where possible.
- Be careful with community nodes: they run code on your server, so install only well-maintained ones.
- Back up the database, data volume and
.envregularly.
Troubleshooting
| Problem | Likely cause and fix |
|---|---|
| No HTTPS certificate | DNS does not point to the server yet, or ports 80/443 are blocked. Check docker compose logs caddy. |
| Webhook URLs show localhost:5678 | N8N_WEBHOOK_URL or N8N_HOST not set; recreate the container with docker compose up -d. |
| n8n restarts in a loop | Database not reachable or wrong password. Check docker compose logs postgres n8n. |
| “Mismatching encryption keys” after a restore | The restored data was created with a different N8N_ENCRYPTION_KEY; restore the original .env. |
| Scheduled workflows run at the wrong time | Set GENERIC_TIMEZONE, or the time zone in the workflow settings. |
Frequently asked questions
Is self-hosted n8n free?
The Community Edition is free to self-host under n8n’s Sustainable Use License, which allows internal business use. Some features (such as SSO and advanced permissions) require a paid plan. Read the licence before offering n8n as a service to others.
Can I use SQLite instead of PostgreSQL?
Yes, SQLite is the default and fine for testing. PostgreSQL is recommended for production because it handles concurrent executions better and is easier to back up and scale.
Docker or npm?
n8n can be installed with npm, but Docker is simpler to update, isolate and back up, and is what n8n’s own hosting guides use.
Need help deploying or securing automation on your servers? See our Linux server hardening & audit service or contact us.
