Deploy n8n Workflow Automation on a Debian VPS with Docker Compose

The first thing you hit after deciding to self-host n8n is the gap between the one-liner in the docs and something you would trust with real workflows. The quick start is fine for a laptop, but on a VPS you want PostgreSQL instead of SQLite, a reverse proxy with automatic HTTPS, and a compose file you can update without breaking your credentials. This guide walks through exactly that on a Debian 12 VPS with Docker Compose v2, the setup that holds up under daily use.
Prerequisites
- A Debian 12 (or 13) VPS with at least 2 GB RAM. n8n plus PostgreSQL plus Caddy fits in 2 GB, but 4 GB gives you headroom for queue mode and heavier workflows.
- Root access or a user with sudo. Every command below assumes sudo.
- A domain name pointing to your VPS IPv4. n8n sits behind HTTPS, and you want a real certificate, not a self-signed one.
- Docker and Docker Compose v2 installed. If they are not installed yet, install them first with the official Docker repository, then continue. I use
docker compose(v2), not the olddocker-composebinary.
Why run n8n on your own Debian VPS at all
n8n is a workflow automation platform where you connect apps, APIs, and databases with a visual editor. You can run hundreds of nodes: HTTP requests, webhooks, database operations, Slack, Gmail, and whatever else you wire together. The cloud version handles the hosting, but self-hosting removes per-execution or per-workflow fees and keeps your workflow data on hardware you control.
For teams that process sensitive data, the argument is simpler. Your workflow credentials, the data passing through the nodes, and the execution logs all stay on your own Linux VPS. No third-party automation vendor sees the payloads. That matters when workflows touch customer records or internal APIs. Run n8n where your data already lives, and you skip one more copy of it in a SaaS database.
越南 VPS 提供本地 IPv4,适合部署 n8n 自动化工作流。
A Vietnam VPS with a local IPv4 is a good fit for hosting n8n automation workflows.
There is also a cost angle. n8n's paid tiers scale with workflows and executions, and once you run a serious volume the self-hosted instance starts to look cheap. On a 2 GB n8n VPS you run the same engine with no per-seat fee. The trade-off is that you own the operations: updates, backups, and uptime are your problem. That is exactly what this guide sets up.
Step 1 - Install Docker and Docker Compose
Debian's own docker.io package is often stale, so install from the official Docker repository. Add the repo, then install the current Docker Engine and the compose plugin.
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/debian/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/debian $(. /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
What this does: it trusts Docker's signing key, points apt at the official stable repository, and installs the engine plus the compose v2 plugin. The plugin gives you the docker compose subcommand, which this whole guide relies on.
Verify both the daemon and the compose plugin are working:
sudo systemctl status docker --no-pager | head -n 5
docker compose version
Expected output: the service shows active (running), and the compose command prints something like Docker Compose version v2.29.x. If the service is not running, start it with sudo systemctl enable --now docker.
One more thing worth doing now: add your user to the docker group so you can run compose without sudo.
sudo usermod -aG docker $USER
newgrp docker
Log out and back in if you want the group change to stick in future sessions. I prefer adding the user to the group over running every compose command with sudo, because file ownership inside bind mounts gets confusing otherwise.
Step 2 - Create the project structure
Keep n8n isolated in its own directory tree. This makes backups trivial and keeps the volume mounts contained. I use /opt/n8n for the compose file and related config, with the actual n8n data in a named volume later.
sudo mkdir -p /opt/n8n
sudo chown $USER:$USER /opt/n8n
cd /opt/n8n
Now create the compose file. This is the core of the whole deployment, so take the time to read each service:
nano docker-compose.yml
Paste the following:
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:2.36.9
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
N8N_DATABASE_TYPE: postgresdb
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: ${DB_USER}
DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD}
N8N_HOST: ${N8N_HOST}
N8N_PROTOCOL: https
N8N_PORT: 5678
WEBHOOK_URL: https://${N8N_HOST}/
volumes:
- n8n_data:/home/node/.n8n
ports:
- "127.0.0.1:5678:5678"
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- n8n
ports:
- "80:80"
- "443:443"
environment:
N8N_HOST: ${N8N_HOST}
volumes:
- caddy_data:/data
- caddy_config:/config
- ./Caddyfile:/etc/caddy/Caddyfile:ro
volumes:
postgres_data:
n8n_data:
caddy_data:
caddy_config:
A few decisions in this file deserve an explanation. n8n binds to 127.0.0.1 only, which means nothing outside the server can reach port 5678 directly; Caddy is the only entry point. The WEBHOOK_URL is set explicitly so n8n generates absolute URLs that match your public domain, which is critical for webhook-based workflows. PostgreSQL runs as a separate container with its own named volume, so a database failure or a n8n upgrade does not touch your workflow data.
Now create the .env file with your secrets and domain. Never commit this file if you use git:
nano .env
DB_USER=n8n
DB_PASSWORD=change_this_strong_password
N8N_HOST=automate.yourdomain.com
Generate a proper password instead of typing one by hand:
openssl rand -hex 24
Copy that output into the DB_PASSWORD value. Using .env keeps secrets out of the compose file itself, which makes it possible to share the compose file or back it up to git without leaking credentials.
Step 3 - Configure Caddy as the reverse proxy
Caddy is the simplest way to get automatic HTTPS. It obtains and renews Let's Encrypt certificates on its own, no certbot, no cron job. Create the Caddyfile in the same directory:
nano Caddyfile
automate.yourdomain.com {
reverse_proxy n8n:5678
}
That is the whole config. Caddy reads the domain, fetches a certificate, and proxies traffic to the n8n service on port 5678. The n8n container name resolves via Docker's internal DNS, so no IP address is needed in the proxy target.
Before starting anything, point your domain's DNS A record at your VPS IPv4 and confirm it resolves:
dig +short automate.yourdomain.com
Expected output: your server's public IP. If it shows something else, wait for DNS propagation or fix the record, because Caddy will fail the certificate challenge until the domain resolves to this server.
Also make sure ports 80 and 443 are open in the firewall. On a Debian VPS with nftables, allow them explicitly:
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
If you use ufw instead, the equivalent is sudo ufw allow 80,443/tcp. Skip this step on a fresh VPS with no firewall configured yet, but verify with sudo nft list ruleset that nothing is blocking.
Step 4 - Start the stack and verify
Everything is in place, so pull the images and start the containers in the background:
cd /opt/n8n
docker compose up -d
First run pulls three images, which takes a minute or two. Watch the startup sequence:
docker compose ps
You want all three services, postgres, n8n, and caddy, in the running state. The n8n container may take ten to twenty seconds to become healthy because it waits for PostgreSQL to accept connections.
Now the real test, open the browser:
https://automate.yourdomain.com
You should see the n8n setup screen asking you to create the owner account. That is the sign that the reverse proxy, the database connection, and n8n itself all work. Complete the owner account and pick a strong password, this account controls the whole instance.
To check the logs if anything looks wrong:
docker compose logs -f n8n
Expected output near the end includes Editor is now accessible via: https://automate.yourdomain.com or a similar line showing the editor URL. If the container restarts in a loop, the logs will tell you why, usually a database connection error or a bad environment variable.
Step 5 - Set up backups for n8n and PostgreSQL
You will not think about backups until a workflow deletes half your data or an upgrade goes sideways, so do it now. The n8n data lives in the n8n_data volume, and the workflows plus credentials live in PostgreSQL. Back up both.
First, a simple database dump to a host directory. Create a backup folder and a small script:
sudo mkdir -p /opt/n8n/backups
cat <<'EOF' | sudo tee /opt/n8n/backup.sh
#!/bin/bash
cd /opt/n8n
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/n8n_db_$(date +%F).sql.gz
docker run --rm -v n8n_n8n_data:/data -v /opt/n8n/backups:/backup alpine tar czf /backup/n8n_files_$(date +%F).tar.gz -C /data .
find backups -type f -mtime +14 -delete
EOF
sudo chmod +x /opt/n8n/backup.sh
Run it once to confirm it works:
sudo /opt/n8n/backup.sh
ls -lh /opt/n8n/backups
Expected output: two files dated today, the compressed SQL dump and the file backup. Then hook it into cron to run daily:
sudo crontab -e
Add this line, adjust the time to whenever you want, and save:
30 2 * * * /opt/n8n/backup.sh
The script keeps the last 14 days of backups and removes older ones. Copy the backup folder to another machine or an object store if you want real disaster recovery; a backup on the same disk protects against user error, not against hardware failure. I push mine to a separate storage bucket nightly.
Troubleshooting common failures
Three failures account for most of what goes wrong with this stack.
n8n restarts in a loop. Almost always a database connection problem. Check the port, the credentials, and the database name in .env. The healthcheck waits for PostgreSQL, but if the password in .env does not match what postgres has, n8n will keep failing. The fix: correct the .env and run docker compose up -d --force-recreate so the containers pick up the new values.
Caddy fails to obtain a certificate. The error logs usually mention ACME or challenge. Most of the time the domain does not resolve to this server, or port 80 is blocked. Verify DNS with dig and confirm port 80 is reachable from outside:
curl -I http://automate.yourdomain.com
Expected output: an HTTP response, often a 302 or a connection reset from Caddy trying to redirect, but not a timeout. Also check docker compose logs caddy for the exact error.
Workflows fail on webhook calls. The URL n8n generates for webhooks must match your public domain. Check Settings, then n8n instance settings in the UI, and confirm the instance URL shows https://automate.yourdomain.com. If it shows an IP or localhost, the WEBHOOK_URL and N8N_HOST in .env are wrong, or the containers were not recreated after changing them.
Updating n8n safely
Updates are where you will appreciate the compose setup. To upgrade n8n, change the image tag in docker-compose.yml, then pull and recreate only the n8n container:
cd /opt/n8n
# edit docker-compose.yml, change the n8n image tag
docker compose pull n8n
docker compose up -d n8n
Your data is safe because it lives in the volumes, not in the container. Run a backup first anyway, then check the release notes for breaking changes. n8n has a solid migration path for the database schema, so an upgrade usually just works. If the new version misbehaves, the previous tag is one edit away.
FAQ
How much RAM does n8n need on a Debian VPS?
2 GB is the practical minimum for n8n with PostgreSQL and Caddy on Debian 12. With 4 GB you can run multiple active workflows and enable queue mode for parallel executions. Workflows doing heavy data processing need more, watch free -h during a busy run to size it.
Why use PostgreSQL instead of SQLite for n8n?
PostgreSQL handles concurrent write access better, which matters when several workflows execute at the same time. SQLite locks the whole database during writes, so under parallel load you get errors. PostgreSQL also gives you a dump format that is easy to back up and restore with standard tools.
Do I need Caddy, or can I use Nginx?
Both work. Caddy is simpler because it obtains and renews certificates automatically with zero extra configuration. Nginx requires certbot or acme.sh plus a proxy config block. If you already run Nginx for other sites on the same VPS, put a location block for n8n in the existing server block instead of adding Caddy.
Can I run n8n with Docker on any Debian version?
Debian 12 and 13 both work fine, and the official Docker repository supports both. The compose file in this guide is version-agnostic, it will run on either. Just make sure your VPS has a recent kernel, which both releases do by default.
How do I restore n8n from the backups?
Start with a fresh compose stack, then restore the database first: gunzip -c n8n_db_DATE.sql.gz | docker compose exec -T postgres psql -U n8n n8n. Then restore the file volume with docker run --rm -v n8n_n8n_data:/data -v /opt/n8n/backups:/backup alpine tar xzf /backup/n8n_files_DATE.tar.gz -C /data. Restart n8n and your workflows and credentials are back.
Related articles
- Deploy n8n with PostgreSQL on a Debian VPS
- How to install n8n on a VPS with Docker in 2026
- How much RAM to run multiple n8n workflows
- Backing up n8n workflows and credentials properly
Debian VPS 部署 n8n 要点
在 Debian 12 VPS 上用 Docker Compose 部署 n8n,使用 PostgreSQL 存储工作流数据,并通过 Caddy 自动配置 HTTPS。n8n 只监听本机端口,所有外部流量经 Caddy 代理,安全性更高。建议每日备份数据库和文件卷,保留 14 天。升级时只改镜像版本并重建容器,数据不会丢失。如果面向越南用户,选择本地 IPv4 的 VPS 可降低访问延迟。


