n8n is a powerful workflow automation tool you can run entirely on your own infrastructure. In this guide, we’ll walk through setting up n8n on a VPS using Docker Compose, PostgreSQL for storage, and Caddy as a reverse proxy with automatic HTTPS.
The result: a secure, production-ready n8n instance available at your own domain.
Assumptions
This guide assumes you already have:
- A VPS with Linux (Ubuntu/Debian/CentOS all fine).
- Docker and Docker Compose installed.
- A domain name with DNS records you can edit.
Directory structure
On your VPS, create a working directory for the stack:
n8n-stack/
├── docker-compose.yml
├── .env
├── init-data.sh
└── Caddyfile
All configuration lives here.
1. Configure environment variables
Create a .env
file with your database credentials:
POSTGRES_USER=postgres_admin
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n_app
POSTGRES_NON_ROOT_PASSWORD=appsecret
This file is used by both Postgres and n8n.
2. Database initialization script
The init-data.sh
script runs once, the first time Postgres initializes. It creates a non-root user for n8n and grants privileges:
#!/bin/bash
set -e;
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No Environment variables given!"
fi
3. Docker Compose setup
docker-compose.yml
defines three services:
- postgres: database storage
- n8n: the workflow engine
- caddy: reverse proxy and HTTPS
version: '3.8'
volumes:
db_storage:
n8n_storage:
caddy_data:
caddy_config:
services:
postgres:
image: postgres:16
restart: always
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
volumes:
- db_storage:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
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
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com
- N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
expose:
- 5678
links:
- postgres
volumes:
- n8n_storage:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
caddy:
image: caddy:2
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
4. Caddy configuration
The Caddyfile
tells Caddy to proxy HTTPS traffic for your domain to the n8n container:
n8n.example.com {
reverse_proxy n8n:5678
}
Caddy automatically requests and renews TLS certificates from Let’s Encrypt.
5. DNS setup
Point your domain to the VPS:
- Add an A record for
n8n.example.com
→ your VPS IPv4. - (Optional) Add an AAAA record for IPv6.
Once DNS resolves, Caddy will handle certificates automatically.
6. Start the stack
Bring everything up:
docker compose up -d
Stop the stack:
docker compose stop
At this point, you should be able to open https://n8n.example.com
in your browser and complete the initial n8n setup.