Business Automation
n8nDockerAutomationDevOpsSecurityBackupsSelf-Hosting

How to Self-Host n8n with Docker in 2026: Security, Backups, and Environment Setup

AO
Adrijan Omičević
·17 min read

# What You'll Learn#

If you want to self host n8n for full control over data, costs, and compliance, you need more than a quick docker run. You need persistence, secrets, SSL, backups, and a security baseline that can survive real production use.

This guide gives you a deployment you can keep running for years: Docker Compose, Postgres, reverse proxy TLS, network isolation, backup and restore, and maintenance routines.

You will also get a security checklist tailored to n8n’s typical risks like exposed webhooks, weak credentials, and overly permissive Docker networking.

ℹ️ Note: If you are still deciding between self-hosting and SaaS automation tools, compare trade-offs in n8n vs Zapier vs Make in 2026. Self-hosting usually wins when you need data residency, custom nodes, or predictable costs at scale.

# Prerequisites#

RequirementRecommendedNotes
Linux VPSUbuntu 22.04 or 24.04Any modern distro works, Ubuntu is easiest to follow
CPU and RAM2 vCPU, 4 GB RAM1 vCPU and 2 GB can work for light usage
Storage40 GB SSDMore if you store binaries and large executions
Docker Engine24+Use official Docker repo packages
Docker Composev2Comes as docker compose plugin on most installs
Domainn8n.yourdomain.comRequired for HTTPS and webhooks
SMTP accountOptionalFor email notifications and password resets
SSH accessKey-based authDisable password login if possible
PortPublicly exposedPurpose
22Yes, restrictedSSH administration
80YesHTTP to HTTPS redirect and ACME challenges
443YesHTTPS for n8n UI and webhooks
5678Non8n internal port, keep private behind reverse proxy
5432NoPostgres internal port, never public

# Architecture Overview: What You’re Deploying#

A production-grade n8n setup typically includes these components:

ComponentWhy it mattersBest practice
n8n containerRuns the editor, webhooks, and executionsRun behind a reverse proxy, not exposed directly
PostgresReliable persistence for workflows and credentials metadataSeparate container, private network, routine dumps
Reverse proxyTLS termination, routing, rate limiting optionsTraefik or Nginx, only 80 and 443 exposed
VolumesPersist n8n config, binaries, logs, and DB dataNamed volumes or bind mounts with backups
SecretsProtect DB passwords, encryption key, webhook URLs.env plus Docker secrets or file-based secrets

🎯 Key Takeaway: Treat n8n like any other web app: a private app container, a private database, and a single hardened public entry point with HTTPS.

# Step 1: Server Hardening Before You Install Anything#

Self-hosting fails most often because the server basics are ignored. Most compromises still come from exposed services, reused passwords, and unpatched packages.

Create a non-root user and lock down SSH#

  1. 1
    Create a new user and add it to sudo.
  2. 2
    Use SSH keys, disable password auth.
  3. 3
    Optionally change SSH port, but do not rely on that alone.
Bash
# create user
adduser deploy
usermod -aG sudo deploy
 
# harden ssh
sudo sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

Firewall baseline with UFW#

Bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

💡 Tip: Restrict SSH to your office IP or VPN range. A simple allowlist eliminates most SSH brute-force noise and reduces log volume significantly.

For a broader checklist you can reuse across projects, see Web Application Security Checklist.

# Step 2: Install Docker and Docker Compose#

Use Docker’s official repository to avoid outdated packages.

Bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
 
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
 
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
 
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Add your deploy user to the docker group:

Bash
sudo usermod -aG docker deploy
newgrp docker
docker version
docker compose version

# Step 3: Create a Clean Project Layout#

A predictable layout makes backups and upgrades safer.

Bash
mkdir -p ~/apps/n8n/{data,db,backups,traefik}
cd ~/apps/n8n

Suggested directory purpose:

PathPurposeBack up
~/apps/n8n/datan8n persistent data and binary storageYes
~/apps/n8n/dbPostgres data directory if bind-mountedYes, or use named volume
~/apps/n8n/backupsLocal backup stagingOptional, but helpful
~/apps/n8n/traefikReverse proxy config and ACME storageYes

# Step 4: Secrets and Environment Setup#

n8n uses an encryption key to protect credentials at rest. If you lose it, you will not be able to decrypt credentials even if you restore the database.

Generate secrets:

Bash
openssl rand -hex 32
openssl rand -base64 48

Create a .env file. Do not commit it to git.

Bash
cd ~/apps/n8n
nano .env

Example .env:

Bash
# domain
N8N_HOST=n8n.yourdomain.com
N8N_PROTOCOL=https
N8N_PORT=5678
WEBHOOK_URL=https://n8n.yourdomain.com/
 
# timezone
GENERIC_TIMEZONE=Europe/Zagreb
TZ=Europe/Zagreb
 
# encryption and auth
N8N_ENCRYPTION_KEY=replace-with-64-hex-or-similar
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=replace-me
N8N_BASIC_AUTH_PASSWORD=replace-me-long-password
 
# database
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=replace-me-long-password
 
# execution and data retention examples
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168

Recommended environment flags to consider:

VariableRecommendedWhy
N8N_ENCRYPTION_KEYSet and store safelyPrevents credential loss on restore
N8N_BASIC_AUTH_ACTIVEEnable at least initiallyAdds an extra gate if your SSO is not set up
WEBHOOK_URLExplicit HTTPS URLPrevents incorrect webhook URLs behind proxy
EXECUTIONS_DATA_PRUNEtrueReduces disk growth and backup size
EXECUTIONS_DATA_MAX_AGE72 to 336 hoursKeeps useful history without infinite retention

⚠️ Warning: Changing N8N_ENCRYPTION_KEY after you already created credentials will effectively lock you out of those credentials. Treat it like a database master key.

# Step 5: Docker Compose with Postgres, n8n, and Traefik SSL#

This example uses Traefik for automatic TLS certificates via Let’s Encrypt. It is a good fit for a single-host VPS deployment.

Create docker-compose.yml:

YAML
services:
  traefik:
    image: traefik:v3.1
    command:
      - --api.dashboard=false
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.le.acme.email=admin@yourdomain.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/letsencrypt
    networks:
      - edge
    restart: unless-stopped
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_POSTGRESDB_USER}
      POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
      POSTGRES_DB: ${DB_POSTGRESDB_DATABASE}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - internal
    restart: unless-stopped
 
  n8n:
    image: n8nio/n8n:latest
    environment:
      N8N_HOST: ${N8N_HOST}
      N8N_PROTOCOL: ${N8N_PROTOCOL}
      N8N_PORT: ${N8N_PORT}
      WEBHOOK_URL: ${WEBHOOK_URL}
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${TZ}
 
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_BASIC_AUTH_ACTIVE: ${N8N_BASIC_AUTH_ACTIVE}
      N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER}
      N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD}
 
      DB_TYPE: ${DB_TYPE}
      DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST}
      DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT}
      DB_POSTGRESDB_DATABASE: ${DB_POSTGRESDB_DATABASE}
      DB_POSTGRESDB_USER: ${DB_POSTGRESDB_USER}
      DB_POSTGRESDB_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
 
      EXECUTIONS_DATA_PRUNE: ${EXECUTIONS_DATA_PRUNE}
      EXECUTIONS_DATA_MAX_AGE: ${EXECUTIONS_DATA_MAX_AGE}
    volumes:
      - n8n_data:/home/node/.n8n
    networks:
      - internal
      - edge
    labels:
      - traefik.enable=true
      - traefik.http.routers.n8n.rule=Host(`${N8N_HOST}`)
      - traefik.http.routers.n8n.entrypoints=websecure
      - traefik.http.routers.n8n.tls.certresolver=le
      - traefik.http.services.n8n.loadbalancer.server.port=5678
    depends_on:
      - postgres
      - traefik
    restart: unless-stopped
 
networks:
  edge:
    name: n8n_edge
  internal:
    name: n8n_internal
    internal: true
 
volumes:
  n8n_data:
  postgres_data:

Bring it up:

Bash
docker compose up -d
docker compose ps
docker logs -n 200 n8n-n8n-1

Confirm you can open:

  • https://n8n.yourdomain.com/ for the UI
  • a test workflow webhook, if you enable one

Why this Compose file is safer than the typical quickstart#

PracticeTypical quickstartThis guide
DatabaseSQLite inside the containerPostgres on a private network
TLSOften missingAutomatic TLS via Traefik
Portsn8n port exposed publiclyOnly 80 and 443 public
Network isolationFlat networkInternal network for DB traffic
PersistenceSometimes ephemeralNamed volumes for DB and n8n data

💡 Tip: Pin versions once stable. For example, n8nio/n8n:1.90.0 instead of latest. It reduces surprise breakage during automatic updates.

# Step 6: Persistence and Data Retention Strategy#

Your two main persistent assets:

  1. 1
    Postgres data volume, which contains workflows, executions metadata, and encrypted credentials blobs.
  2. 2
    n8n data volume, which contains config and binary data, including file uploads depending on settings.

Keep executions under control#

Unbounded execution history causes two problems: disk growth and longer backups. If you run 5,000 executions per day and each stores even 50 KB of JSON, that is about 5000 * 50 KB = 250 MB per day, or roughly 7.5 GB per month.

Pruning settings in .env reduce this significantly. A common baseline is 7 days for debugging and audits, or 72 hours if you have separate logging and monitoring.

# Step 7: Secrets Management Options That Actually Work#

If you truly want to self host n8n long-term, .env is only step one. You need a plan for where secrets live, who can read them, and how they are rotated.

Option A: .env with strict file permissions#

This is the minimum viable approach.

Bash
chmod 600 ~/apps/n8n/.env
chown deploy:deploy ~/apps/n8n/.env

Pros: simple. Cons: secrets live on disk in plain text.

Option B: Docker secrets with file-based environment values#

Docker Compose can mount secrets as files, and many services can read from files. n8n itself primarily expects env vars, so the practical pattern is a wrapper entrypoint or exporting variables from files.

If you want a simpler hardening approach, keep .env but store the same values in a password manager and rotate them periodically.

Option C: External secrets manager#

For regulated environments, use a secrets manager and inject at runtime. The key requirement is that the encryption key and DB password are retrievable consistently on every restart.

ℹ️ Note: Regardless of method, your top priority secrets are N8N_ENCRYPTION_KEY, database password, and any n8n credentials used for production systems like payment providers and CRM admins.

# Step 8: SSL Details and Common Reverse Proxy Issues#

Traefik terminates TLS and forwards to n8n. Two settings prevent 80 percent of “webhook URL” issues:

  • Set WEBHOOK_URL to your public HTTPS URL.
  • Ensure the Host header routing matches your domain.

Add an HTTP to HTTPS redirect#

Add these labels to the n8n router or configure Traefik global redirect. A simple pattern is a dedicated HTTP router that redirects.

If you want a clean minimal change, add this to the Traefik command list:

YAML
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https

# Step 9: Security Hardening for n8n in Production#

Self-hosting changes your threat model. Attackers will try credential stuffing, exploit exposed webhooks, and probe admin panels.

1) Authentication and permissions#

At a minimum:

  • Use n8n user management and strong passwords.
  • Keep basic auth enabled until you have SSO or network controls.
  • Use separate n8n accounts per person, not shared credentials.

If your n8n instance supports roles, avoid giving everyone admin access. The main risk is accidental credential exposure and workflow edits.

2) Network isolation and inbound exposure#

The Compose file uses an internal network for Postgres. That prevents the DB container from being reachable from other Docker networks and from the host’s external interfaces.

Also ensure you are not publishing Postgres ports. Do not add ports for Postgres unless you have a very specific need.

3) Webhook exposure and allowlisting#

If you create public webhooks, they are internet-facing endpoints. Protect them like any API:

  • Use secrets in the webhook path or headers.
  • Validate payload signatures where supported.
  • Rate-limit at the reverse proxy if possible.

A typical secure pattern is a header secret that the first node validates before doing anything expensive.

⚠️ Warning: “Hidden” webhook URLs are not authentication. If the URL leaks into logs, browser history, or a ticket, it becomes a permanent bypass.

4) Container permissions and filesystem#

Avoid running extra privileges:

  • Do not run n8n with privileged: true.
  • Do not mount the Docker socket into the n8n container.
  • Prefer named volumes over binding sensitive host paths.

If you bind-mount directories, keep ownership tight and avoid world-writable permissions.

5) Security headers and app-level controls#

Reverse proxies can add headers like HSTS and X-Content-Type-Options. Start with the general practices in our Web Application Security Checklist and apply them to your proxy layer.

6) Keep dependencies updated, but not blindly#

Updating n8n is security-critical because automation tools touch many systems. However, updates can break workflows due to node changes.

A practical cadence:

ItemCadenceHow
OS security updatesWeeklyunattended-upgrades or scheduled maintenance
Docker and engineMonthlypin and upgrade in a window
n8n versionEvery 2 to 4 weeksread release notes, test on staging
Postgres minor updatesMonthlykeep within major version

# Step 10: Backups You Can Restore#

A backup is only real if you can restore it. For n8n, treat it like you would a production SaaS: database dump plus data directory snapshot, automated daily, stored offsite, and tested monthly.

What to back up#

AssetContainsBackup method
Postgres databaseWorkflows, users, credentials blobs, execution recordspg_dump logical backups and occasional volume snapshots
n8n data volumeInstance config, binary data, some settingsvolume archive or bind-mount tarball
.envencryption key, DB password, hostnamesstore securely, version with secret manager
Reverse proxy ACMETLS cert metadataoptional, can be reissued

Backup script: Postgres plus n8n data#

Create backup.sh:

Bash
#!/usr/bin/env bash
set -euo pipefail
 
BASE_DIR="$HOME/apps/n8n"
BACKUP_DIR="$BASE_DIR/backups"
TS="$(date +%F_%H-%M)"
mkdir -p "$BACKUP_DIR"
 
# Postgres logical dump
docker exec -t n8n-postgres-1 pg_dump -U n8n n8n | gzip > "$BACKUP_DIR/postgres_$TS.sql.gz"
 
# n8n volume archive
docker run --rm \
  -v n8n_n8n_data:/volume:ro \
  -v "$BACKUP_DIR":/backup \
  alpine:3.20 \
  sh -c "cd /volume && tar -czf /backup/n8n_data_$TS.tar.gz ."
 
# optional: checksum
sha256sum "$BACKUP_DIR"/*"$TS"* > "$BACKUP_DIR/checksums_$TS.txt"

Make it executable:

Bash
chmod +x ~/apps/n8n/backup.sh

Schedule daily at 02:30:

Bash
crontab -e

Add:

Bash
30 2 * * * /home/deploy/apps/n8n/backup.sh >/home/deploy/apps/n8n/backups/cron.log 2>&1

💡 Tip: Apply a retention policy. A common baseline is 14 daily backups plus 8 weekly backups. Without retention, backups eventually become the outage.

Offsite storage#

At minimum, copy backups off the VPS daily. Options include S3-compatible storage, Backblaze B2, or another server.

If you use rclone, you can push the backup folder after creation. Keep credentials restricted to only that bucket and path.

# Step 11: Restore Procedure (Test This on Staging)#

Restoring n8n is straightforward if you keep the encryption key and restore in the right order.

Restore checklist#

StepActionWhy
1Stop servicesPrevent writes during restore
2Restore Postgres dumpBrings back workflows and credentials blobs
3Restore n8n data volumeRestores config and binaries
4Ensure .env includes original encryption keyEnables credential decryption
5Start services and validateConfirms workflows, webhooks, and executions

Stop containers:

Bash
cd ~/apps/n8n
docker compose down

Restore Postgres:

Bash
gzip -dc backups/postgres_YYYY-MM-DD_HH-MM.sql.gz | \
docker run -i --rm --network n8n_internal postgres:16-alpine \
psql "postgresql://n8n:YOURPASSWORD@postgres:5432/n8n"

Restore n8n volume archive:

Bash
docker run --rm \
  -v n8n_n8n_data:/volume \
  -v "$HOME/apps/n8n/backups":/backup \
  alpine:3.20 \
  sh -c "rm -rf /volume/* && cd /volume && tar -xzf /backup/n8n_data_YYYY-MM-DD_HH-MM.tar.gz"

Start again:

Bash
docker compose up -d
docker compose logs -n 200 n8n

Validate restore#

  • Can you log in to the UI
  • Do workflows appear
  • Do credentials show as valid and not “missing encryption key”
  • Do at least two critical workflows run successfully
  • Do webhooks return the expected status codes

⚠️ Warning: A successful restore that cannot decrypt credentials is still a failed restore. Your restore drill must include credential validation.

# Step 12: Maintenance Best Practices#

A self-hosted automation platform is part of your production stack. Maintenance is what makes it cheaper than SaaS long-term.

Update routine you can keep#

  1. 1
    Clone your production Compose directory to staging.
  2. 2
    Upgrade n8n image version in staging and run workflow smoke tests.
  3. 3
    Schedule a maintenance window.
  4. 4
    Backup first, then upgrade production.

Upgrade example:

Bash
cd ~/apps/n8n
docker compose pull
docker compose up -d
docker compose ps

Monitor the right signals#

SignalWhat to watchTooling options
Diskvolumes and logs growingdf -h, log rotation, pruning
MemoryOOM kills during executionsdocker stats, VPS metrics
Queue latencyexecutions backloggedn8n executions view, external monitoring
Errorsworkflow failures, credential issuesemail alerts, log aggregation

Log growth and retention#

Containers can generate large logs over months. Ensure Docker log rotation is enabled on the host. A common safe baseline is 10 MB per file times 3 files.

Database health#

If you run many executions, Postgres performance matters. At minimum:

  • Keep enough disk headroom, target 20 percent free space.
  • Vacuum is handled automatically, but monitor bloat if you store many execution records.
  • Consider pruning execution data more aggressively if you do not need long retention.

# When Self-Hosting n8n Is Worth It#

Self-hosting has a cost, but it also removes a lot of variable pricing. Teams often switch to self host n8n when:

  • They run high-volume workflows and want predictable infrastructure spend.
  • They need data residency or private networking to internal services.
  • They want custom nodes or deeper integrations.

If you need help choosing the best approach, start with n8n vs Zapier vs Make in 2026 and then map it to your data and compliance requirements.

If you want this deployed, secured, and monitored without burning internal engineering time, see our automation services.

# Key Takeaways#

  • Put n8n behind a reverse proxy with HTTPS and expose only ports 80 and 443 publicly.
  • Use Postgres on a private Docker network and never publish database ports to the internet.
  • Store and protect N8N_ENCRYPTION_KEY like a master key, and include it in your disaster recovery plan.
  • Back up both Postgres and the n8n data volume, automate daily, enforce retention, and test restores monthly.
  • Reduce operational risk by pinning versions, upgrading on a schedule, and validating critical workflows after every update.

# Conclusion#

To self host n8n safely, you need a production mindset: private networking, strong authentication, TLS, reliable backups, and a repeatable upgrade process. Once those pieces are in place, n8n becomes a stable automation backbone that can scale with your business without SaaS pricing surprises.

If you want Samioda to deploy this setup for you, harden it, and add monitoring and automated backups, contact us via our automation services page.

FAQ

Share
A
Adrijan OmičevićSamioda Team
All articles →

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.