• Post author:Bhaskar Soni
  • Post published:
  • Post category:DevOps / Linux
  • Post comments:0 Comments
  • Reading time:7 mins read

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.

n8n

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 version

Step 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 .env

Replace 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:

SettingPurpose
N8N_HOST, N8N_PROTOCOLThe public host name and protocol of your instance
N8N_WEBHOOK_URLPublic 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_KEYEncrypts stored credentials; must stay the same forever
GENERIC_TIMEZONE, TZTime zone for Schedule triggers and system time
EXECUTIONS_DATA_PRUNE, EXECUTIONS_DATA_MAX_AGEDelete execution history older than 336 hours (14 days) so the database does not grow forever
No ports on n8nn8n 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/healthz

Open 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:

  1. Schedule Trigger: run every 5 minutes.
  2. 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.
  3. If: check whether the status code is not equal to 200.
  4. 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 -f

Watch 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 ports entry 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 .env regularly.

Troubleshooting

ProblemLikely cause and fix
No HTTPS certificateDNS does not point to the server yet, or ports 80/443 are blocked. Check docker compose logs caddy.
Webhook URLs show localhost:5678N8N_WEBHOOK_URL or N8N_HOST not set; recreate the container with docker compose up -d.
n8n restarts in a loopDatabase not reachable or wrong password. Check docker compose logs postgres n8n.
“Mismatching encryption keys” after a restoreThe restored data was created with a different N8N_ENCRYPTION_KEY; restore the original .env.
Scheduled workflows run at the wrong timeSet 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.

Sources

Post author avatar

Bhaskar Soni

Bhaskar Soni is a Security Engineer and the founder of Youngster Company, based in Ahmedabad, Gujarat. He works hands-on with Linux server hardening, vulnerability assessment and penetration testing (VAPT), ISO 27001 compliance and OSINT investigations. He writes practical, tested guides for IT professionals and Indian businesses, and every article on this site is written or reviewed by him.

Leave a Reply