AI Automation

Deploy and secure n8n on a Debian VPS in Vietnam

You have a fresh Debian 12 or 13 VPS, and you want to self-host n8n for workflow automation. The default docker run command from the docs works, but it leaves n8n exposed on port 5678 with no TLS, no database tuning, and no firewall. For anything that touches real data, that is not good enough. This guide walks through deploying n8n with Docker Compose and PostgreSQL on a Debian VPS, then securing it with Nginx, HTTPS, and a firewall. Every step gives you a command and a verify step, so you can copy-paste and know it worked.

越南 VPS 提供本地 IPv4,适合部署需要稳定入站连接的自动化服务。

A Vietnam VPS gives you a local IPv4, which suits automation services that need stable inbound connections.

Prerequisites

  • A Debian 12 or 13 VPS, with root or sudo access.
  • A domain name pointing to your VPS IPv4, or a subdomain like n8n.example.com.
  • Basic familiarity with SSH and the command line.
  • At least 2 GB of RAM. n8n plus PostgreSQL and Nginx fits in 2 GB, but 4 GB makes heavy workflows more comfortable.

If you are looking for a machine to run this on, a Linux VPS with a dedicated IPv4 in Vietnam and NVMe storage works well. n8n is IO-heavy during workflow startup, and NVMe keeps the latency down.

Why run n8n on a Debian VPS with Docker

n8n is a Node.js application with a web editor, a queue mode for scaling, and a webhook endpoint for inbound triggers. Running it directly on the host works, but Docker Compose gives you three things: a clean separation of services, a reproducible setup, and easy upgrades. You pin the image version, back up the compose file, and you can move the whole stack to another VPS in minutes.

PostgreSQL is the right database for n8n in production. The default SQLite backend is fine for a single low-volume instance, but it locks under concurrent writes and makes backups trickier. PostgreSQL handles the concurrency that webhook-triggered workflows produce. This guide uses PostgreSQL 16 from the Debian repository, or you can run it as a container if you prefer.

The other reason to self-host is control over the network. When your n8n instance sits on a n8n VPS with a dedicated IPv4, you decide what inbound traffic reaches it, and you keep credentials on your own infrastructure instead of on a SaaS vendor. For teams that process customer data, that distinction matters.

Step 1 - Installing Docker and Docker Compose

Start by updating the package index and installing the dependencies Docker needs.

apt update
apt install -y ca-certificates curl gnupg

Add the official Docker GPG key and repository. Debian 12 and 13 both use the bookworm or trixie codename, but the command below uses the generic $(. /etc/os-release && echo "$VERSION_CODENAME") so it adapts to your release.

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
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" | tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine, the CLI, and the Compose plugin. On 2026 systems the modern syntax is docker compose (v2), not the legacy docker-compose binary.

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify the installation.

docker --version
docker compose version

Expected output shows Docker 27.x or newer and the Compose v2 plugin. If the commands are not found, check that the Docker repository was added correctly with cat /etc/apt/sources.list.d/docker.list.

Step 2 - Setting up the directory structure and environment

Create a directory for the n8n stack and a subdirectory for the PostgreSQL data volume.

mkdir -p /opt/n8n/postgres-data
cd /opt/n8n

Now create an environment file that holds the secrets. Never put credentials in the compose file itself, because the compose file gets committed to git or copied between machines.

nano /opt/n8n/.env

Paste the following, and replace the values with strong random strings. You can generate them with openssl rand -base64 24.

POSTGRES_USER=n8n
POSTGRES_PASSWORD=change-me-strong-password
POSTGRES_DB=n8n
N8N_ENCRYPTION_KEY=change-me-32-char-minimum
N8N_HOST=n8n.example.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.example.com/

The encryption key is critical. n8n uses it to encrypt credentials stored in the database. If you lose it, you cannot decrypt saved credentials, even with a valid database backup. Store this file in a safe place, outside the VPS if possible.

Step 3 - Creating the Docker Compose file

Create the compose file for the n8n stack.

nano /opt/n8n/docker-compose.yml

Here is a production-oriented setup with PostgreSQL as a container, n8n as the application, and a restart policy so the services come back after a reboot or crash.

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - /opt/n8n/postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  n8n:
    image: n8nio/n8n:latest
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_HOST: ${N8N_HOST}
      N8N_PROTOCOL: ${N8N_PROTOCOL}
      WEBHOOK_URL: ${WEBHOOK_URL}
      N8N_RUNNERS_ENABLED: "true"
    ports:
      - "127.0.0.1:5678:5678"

Two details matter here. First, the n8n port is bound to 127.0.0.1, so it is not reachable from the internet. Only Nginx, which runs on the same host, can talk to it. Second, the PostgreSQL healthcheck ensures n8n does not start before the database is ready, which avoids the transient connection errors you see when both containers start at once.

The environment variable N8N_RUNNERS_ENABLED is set to true, which enables the code runner for Code node and Python nodes in newer n8n versions. If your n8n build does not support it, the compose file still works; the variable is ignored.

Pull the images and start the stack.

docker compose up -d

Verify that both containers are running.

docker compose ps

You should see both postgres and n8n with a status of "running" or "healthy". Then confirm n8n responds locally.

curl -I http://127.0.0.1:5678

Expected output is an HTTP 200 or 302 response from n8n. This confirms the application is alive but not yet publicly reachable.

Step 4 - Configuring Nginx as a reverse proxy with HTTPS

Exposing n8n directly on port 5678 with plain HTTP is a bad idea, because credentials and webhooks travel unencrypted. Install Nginx and Certbot instead.

apt install -y nginx certbot python3-certbot-nginx

Create a site configuration for your n8n subdomain.

nano /etc/nginx/sites-available/n8n

Paste this server block, replacing n8n.example.com with your domain.

server {
    listen 80;
    server_name n8n.example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name n8n.example.com;

    ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;

    client_max_body_size 20m;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

The Upgrade and Connection headers are required for the n8n webhook editor to work correctly over a proxy, especially when testing webhooks in the browser. Without them, some polling requests hang.

Enable the site and get the SSL certificate.

ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
certbot --nginx -d n8n.example.com

Certbot detects the server block, places the certificate, and updates the Nginx configuration automatically. It also installs a renewal timer, which you can verify with systemctl list-timers | grep certbot.

Verify the HTTPS endpoint.

curl -I https://n8n.example.com

Expected output is an HTTP 301 or 200 from n8n with a valid certificate. If you see an SSL error, run certbot certificates to confirm the certificate was issued.

Step 5 - Locking down the firewall

The Debian VPS defaults to no firewall, which means every open port is exposed. Install and configure UFW to allow only SSH, HTTP, and HTTPS.

apt install -y ufw
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Check the firewall status.

ufw status verbose

Expected output lists OpenSSH on port 22, HTTP on 80, and HTTPS on 443 as allowed. Port 5678 should not be listed, because n8n is bound to localhost and does not need a firewall rule. If you get a warning about SSH being blocked, allow the SSH port first with ufw allow 22/tcp before enabling.

For teams that collaborate on workflows, you might also want to restrict SSH to specific source IPs. A common pattern is ufw allow from 203.0.113.0/24 to any port 22, which narrows administrative access to your office or VPN range. This reduces the attack surface significantly on a public IPv4.

Step 6 - Verifying the full setup

Open your browser and go to https://n8n.example.com. You should see the n8n owner setup page, where you create an admin account. After that, the editor loads. Create a simple test workflow with a Manual Trigger and a Set node to confirm everything works end to end.

Then test a webhook trigger, because inbound webhooks are the main reason you need a public HTTPS endpoint. n8n shows the production webhook URL in the editor, and hitting that URL with curl should execute the workflow.

curl -X POST https://n8n.example.com/webhook-test/your-workflow-id

Expected output is a confirmation from the webhook node. If the webhook returns a 404, the workflow is not active or the URL has the test prefix. Activate the workflow and use the production URL instead.

Troubleshooting common issues

One common issue is n8n redirecting to http://localhost:5678 when you open the editor. This happens when N8N_HOST is not set or does not match the public hostname. Check the environment file and restart the container with docker compose up -d after fixing it.

Another frequent failure is the "connection refused" error from PostgreSQL. Run docker compose logs postgres to see if the database started correctly. A missing or wrong POSTGRES_PASSWORD environment variable is the usual cause, because the database rejects authentication.

A third issue is webhooks timing out in the editor. This is almost always the missing Upgrade and Connection headers in Nginx. Confirm they are present in the configuration and reload Nginx with nginx -t && systemctl reload nginx.

Backups: protect the n8n data directory

Backing up n8n is not just dumping the PostgreSQL database. You also need the .env file, because it holds the encryption key. Without it, a database restore produces unusable credentials. A minimal backup strategy covers three things: the database dump, the environment file, and the compose file.

docker compose exec postgres pg_dump -U n8n n8n > /opt/n8n-backup/n8n-$(date +%F).sql

Copy the dump and the environment file off the VPS to a separate location. If you use snapshots, take one before upgrading n8n. The n8n VPS offering includes snapshot and backup options, which gives you a quick rollback point before a version bump.

For the upgrade itself, pull the new image and recreate the containers.

docker compose pull n8n
docker compose up -d

n8n runs database migrations automatically on startup. Check the logs after the upgrade.

docker compose logs n8n | tail -50

Expected output shows n8n starting and any migration messages completing without errors.

FAQ

What is the minimum VPS configuration for n8n?

A 2 GB RAM VPS with 2 vCPUs runs n8n, PostgreSQL, and Nginx comfortably for light to moderate workflow volume. If you run many parallel workflows or use the queue mode with multiple workers, move to a 4 GB or 8 GB plan. The database buffer and the n8n process are the main memory consumers, not the code execution itself.

Why bind n8n to localhost instead of exposing port 5678?

Binding to 127.0.0.1 means n8n is only reachable from the VPS itself. Nginx, which terminates HTTPS and handles the public traffic, proxies to it. This removes the risk of running an unencrypted service on a public IPv4 and keeps the firewall rules minimal: only 22, 80, and 443 are open.

How do I update n8n without losing my workflows?

Pull the new image with docker compose pull n8n and recreate the container with docker compose up -d. n8n runs migrations automatically. Before upgrading, take a PostgreSQL dump and snapshot the VPS, then verify the new version with a test workflow.

What happens if I lose the N8N_ENCRYPTION_KEY?

n8n uses this key to encrypt all credentials stored in its database. If you lose it, the credentials cannot be decrypted, even if the database is intact. You would need to re-enter every credential manually. Store the .env file in at least two places outside the VPS.

Is the Docker Compose approach better than the npm install method?

For production, yes. Compose gives you a clean service boundary, a defined database, and reproducible startup. The npm method puts n8n directly on the host, which mixes Node.js dependencies with the system packages and makes rollbacks harder. The queue mode for horizontal scaling also relies on the same images and configuration that Compose provides.

Related articles

Debian VPS 部署 n8n 的安全要点

本文介绍了在 Debian VPS 上用 Docker Compose 和 PostgreSQL 部署 n8n 的完整流程,并通过 Nginx 反向代理和 HTTPS 加密保护服务。关键安全措施包括将 n8n 绑定到本机地址、使用 UFW 只开放 22/80/443 端口、妥善保管加密密钥。建议备份数据库、环境文件和快照后再升级。选择越南本地 IPv4 可以保证入站 webhook 连接的稳定性。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.