👍 What we like
- ✓Self-hosted with no execution limits or operation costs
- ✓Data stays private and never passes through third-party clouds
- ✓Automatic HTTPS and Let's Encrypt via Caddy reverse proxy
- ✓Connects hundreds of services including APIs, databases, and AI
👎 What to watch
- ✕Requires manual configuration of DNS, Docker, and encryption keys
- ✕Sensitive API credentials demand strict security and backup practices
- ✕PostgreSQL is mandatory for production, not just SQLite
- ✕Encryption key loss makes all stored credentials unreadable
📑 Contents ▾
- 01 Prerequisites
- 02 Step 1: Install Docker and Docker Compose
- 03 Step 2: Configure DNS
- 04 Step 3: Create the shared Docker network
- 05 Step 4: Prepare the folder and .env file
- 06 Step 5: The n8n docker-compose.yml
- 07 Step 6: The Caddyfile for automatic HTTPS
- 08 Step 7: Start the services
- 09 Step 8: Create the owner account and first workflow
- 10 Step 9: Backups
- 11 Step 10: Final verification
- 12 Security and Hardening
- 13 Common Pitfalls and Troubleshooting
- 14 FAQ
- · Is self-hosted n8n really free?
- · Why use PostgreSQL instead of the default SQLite database?
- · What happens if I lose my encryption key?
- · Why don’t my webhooks work with external services?
- · Can I run AI in my n8n workflows?
- · n8n, Activepieces, or Node-RED: which one to choose?
- 21 Related Topics
You’re chaining repetitive tasks across multiple apps and paying Zapier or Make based on operation volume? n8n changes the game. It’s an open-source, self-hostable workflow automation platform that connects hundreds of services (APIs, databases, webhooks, AI) in visual scenarios. When self-hosted, you have no execution limits, your data never passes through a third-party cloud, and the cost is reduced to just your server.
However, n8n handles your most sensitive API credentials (Stripe keys, Google tokens, database access…). It must therefore be deployed with mandatory HTTPS, a reliable PostgreSQL database instead of SQLite, a persistent encryption key, and serious backups. In this tutorial, we deploy n8n via Docker Compose with PostgreSQL, behind a Caddy reverse proxy that handles HTTPS and Let’s Encrypt automatically.
Prerequisites
- A secure VPS running Ubuntu 24.04 (or Debian 12), with 2 GB of RAM sufficient to start. If you haven’t already, follow our guide to install and secure an Ubuntu VPS. A small VPS from Hetzner or Infomaniak is more than enough.
- Docker and Docker Compose installed (command in step 1).
- A domain name for which you control the DNS. We will use
n8n.exemple.fr. A domain is essential: many third-party services refuse to register webhooks pointing to a bare IP address. - Ports 80 and 443 open for Let’s Encrypt and HTTPS traffic.
Step 1: Install Docker and Docker Compose
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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 update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
Log out and log back in, then verify:
docker --version && docker compose version
Step 2: Configure DNS
At your DNS provider, create an A record (and AAAA if IPv6):
| Type | Name | Value |
|---|---|---|
| A | n8n.exemple.fr | 203.0.113.10 |
Verify propagation:
dig +short n8n.exemple.fr
Step 3: Create the shared Docker network
docker network create web
If you are already following our Caddy reverse proxy tutorial, this network might already exist: ignore any error message.
Step 4: Prepare the folder and .env file
n8n strictly requires a persistent encryption key: this is what encrypts your stored credentials. If it changes, all your saved credentials become unreadable. We generate it once and freeze it in .env.
mkdir -p ~/n8n && cd ~/n8n
nano .env
# --- Domain ---
N8N_HOST=n8n.exemple.fr
N8N_PROTOCOL=https
# Full public URL for webhooks (no trailing slash)
WEBHOOK_URL=https://n8n.exemple.fr/
# --- Encryption Key (NEVER change it after the first startup) ---
N8N_ENCRYPTION_KEY=replace_with_generated_key
# --- PostgreSQL Database ---
POSTGRES_USER=n8n
POSTGRES_PASSWORD=replace_with_a_long_password
POSTGRES_DB=n8n
# --- Miscellaneous ---
GENERIC_TIMEZONE=Europe/Paris
TZ=Europe/Paris
Generate the encryption key and a database password:
echo "N8N_ENCRYPTION_KEY : $(openssl rand -hex 24)"
echo "POSTGRES_PASSWORD : $(openssl rand -base64 32)"
Record these values in .env. Note the encryption key in your password manager: you will need it to restore a backup on another server.
Step 5: The n8n docker-compose.yml
nano docker-compose.yml
services:
postgres:
image: postgres:16-alpine
container_name: n8n_postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- n8n_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- n8n-internal
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
environment:
# --- PostgreSQL Connection ---
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}
# --- Domain and Webhooks ---
N8N_HOST: ${N8N_HOST}
N8N_PROTOCOL: ${N8N_PROTOCOL}
N8N_PORT: 5678
WEBHOOK_URL: ${WEBHOOK_URL}
N8N_EDITOR_BASE_URL: ${WEBHOOK_URL}
# --- Security ---
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_SECURE_COOKIE: "true"
# Essential behind a reverse proxy: trusts the X-Forwarded header
N8N_PROXY_HOPS: 1
# --- Miscellaneous ---
GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
TZ: ${TZ}
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
networks:
- web
- n8n-internal
volumes:
n8n_pgdata:
n8n_data:
networks:
web:
external: true
n8n-internal:
The n8n service is on two networks: n8n-internal to reach PostgreSQL, and web to be reached by Caddy. PostgreSQL is only on the internal network. No ports: section: n8n is never exposed directly. The N8N_PROXY_HOPS: 1 parameter is crucial behind a reverse proxy; otherwise, n8n complains about the X-Forwarded-For header.
Step 6: The Caddyfile for automatic HTTPS
If you already have a Caddy setup, add the site block. Otherwise, create a Caddy folder:
mkdir -p ~/caddy && cd ~/caddy
nano Caddyfile
{
email admin@exemple.fr
}
n8n.exemple.fr {
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
reverse_proxy n8n:5678
}
The internal port of n8n is 5678. Caddy natively handles the WebSockets used by the workflow editor.
Then the Caddy docker-compose.yml:
nano docker-compose.yml
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web
volumes:
caddy_data:
caddy_config:
networks:
web:
external: true
Step 7: Start the services
cd ~/n8n && docker compose up -d
cd ~/caddy && docker compose up -d
Follow the logs to verify the database connection and certificate issuance:
cd ~/n8n && docker compose logs -f n8n
When n8n displays Editor is now accessible via ..., visit https://n8n.exemple.fr. The owner account creation screen will appear.
Step 8: Create the owner account and first workflow
- Enter an email and a strong password: this account is the owner of the instance.
- Create a simple first workflow to validate everything, for example, a “Manual” trigger followed by an “HTTP Request” node pointing to a public API, and execute it.
- Test a webhook: create a workflow with a “Webhook” node, activate it, and call the provided URL (it correctly uses
https://n8n.exemple.fr/...thanks toWEBHOOK_URL). This is the decisive test: if the webhook responds over HTTPS, your deployment is correct.
Then add your credentials (API keys for the services to automate): they are encrypted with your N8N_ENCRYPTION_KEY before being stored in the database.
Step 9: Backups
All the useful state of n8n lives in PostgreSQL (workflows, encrypted credentials, executions). The encryption key, however, is in .env and the n8n_data volume. Back up both.
nano ~/n8n/backup.sh
#!/bin/bash
set -euo pipefail
cd "$HOME/n8n"
BACKUP_DIR="$HOME/n8n/backups"
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
# Consistent dump of the PostgreSQL database
docker compose exec -T postgres \
pg_dump -U n8n -d n8n | gzip > "$BACKUP_DIR/n8n-db-$STAMP.sql.gz"
# Backup of the .env file (contains the encryption key — keep it secure!)
cp .env "$BACKUP_DIR/.env-$STAMP"
# Keep 14 days
find "$BACKUP_DIR" -name 'n8n-db-*.sql.gz' -mtime +14 -delete
find "$BACKUP_DIR" -name '.env-*' -mtime +14 -delete
chmod +x ~/n8n/backup.sh
(crontab -l 2>/dev/null; echo "0 3 * * * $HOME/n8n/backup.sh") | crontab -
A database backup without the encryption key is unusable: you could restore the workflows but not decrypt the credentials. Send these backups off-site and encrypted using our guide automatic backup with Restic and Backblaze.
Step 10: Final verification
# Containers are running and the database is healthy
docker compose -f ~/n8n/docker-compose.yml ps
# HTTP redirects to HTTPS (code 308)
curl -sI http://n8n.exemple.fr | head -1
# HTTPS responds
curl -sI https://n8n.exemple.fr | head -1
Security and Hardening
- Strict UFW firewall. Open only ports 80, 443, and SSH. The n8n port 5678 and PostgreSQL port 5432 must never be exposed — the network configuration handles this already.
- Strong owner password and, if you manage a team, enable two-factor authentication and separate user accounts.
- Protected encryption key. The
.envfile contains secrets:chmod 600 .envand never commit it to version control. - Controlled updates. n8n releases frequently. Back up first, then
docker compose pull && docker compose up -d. Watch release notes for breaking changes. - Public webhooks. Any workflow exposing a webhook is publicly accessible: secure them with authentication (header, token) in the Webhook node.
To harden the VPS, follow install and secure an Ubuntu VPS.
Common Pitfalls and Troubleshooting
- “Your encryption key has changed”. You modified or lost
N8N_ENCRYPTION_KEY. Restore the original key from your backup, otherwise the encrypted credentials are unrecoverable. - Proxy error / insecure cookie.
N8N_PROXY_HOPSorN8N_SECURE_COOKIEis missing. Check these variables and ensureN8N_PROTOCOLis set tohttps. - Webhooks point to
localhostor the IP.WEBHOOK_URLis not correctly defined. It must be the full public HTTPS URL. - n8n cannot connect to the database. Check PostgreSQL credentials and ensure the
postgresservice ishealthy(docker compose ps). Thedepends_on … condition: service_healthyprevents premature startups.
FAQ
Is self-hosted n8n really free?
The Community version of n8n is free and has no execution limits: you only pay for your server. Some advanced features (enterprise SSO, environments, certain collaboration options) belong to a paid Enterprise offer, but for automating personal workflows or those of a small team, the Community version covers the vast majority of needs.
Why use PostgreSQL instead of the default SQLite database?
SQLite works for testing, but quickly shows its limits with concurrent executions and large volumes of history. PostgreSQL handles load, hot backups, and data growth much better. For an instance you plan to use seriously, PostgreSQL is the right choice from the start.
What happens if I lose my encryption key?
Your workflows remain recoverable, but all saved credentials become unreadable: you will have to re-enter them one by one. This is why the key must be noted in your password manager and backed up with the database. Never regenerate it on an existing instance.
Why don’t my webhooks work with external services?
Most often, it’s because WEBHOOK_URL does not reflect the public HTTPS URL, or the domain is not reachable from the Internet. Many services (Stripe, GitHub…) refuse to register a webhook pointing to a bare IP or over HTTP. The HTTPS reverse proxy in this tutorial solves this problem.
Can I run AI in my n8n workflows?
Yes. n8n includes nodes for LLMs (OpenAI, local models via Ollama, AI agents). When self-hosted, you can even connect n8n to a local model to keep all data on-premise. However, be mindful of RAM: AI workflows and parallel executions consume more resources.
n8n, Activepieces, or Node-RED: which one to choose?
n8n offers the best balance between power, number of integrations, and ease of use; Activepieces aims for simplicity; Node-RED is king for home automation and IoT. For automating web applications and APIs, n8n is generally the most versatile. Our comparison n8n vs Activepieces vs Node-RED details each profile.
Related Topics
- n8n vs Activepieces vs Node-RED: which automation platform
- Hosting n8n on a VPS
- Automatic HTTPS reverse proxy with Caddy and Docker
- Install and secure an Ubuntu VPS from A to Z
You now have an n8n instance over HTTPS, backed by PostgreSQL, with persisted and backed-up encrypted workflows and credentials — with no execution limits. It’s the tool that automates your entire homelab and daily tasks. To follow n8n updates, AI nodes, and best practices for self-hosting, subscribe to our Telegram watch bot.