I learned this the hard way. The first time I put n8n in production I left Redis listening on 0.0.0.0
with no password because it “was just for a minute.” A week later I saw connection attempts from random hosts. Nothing happened, but it was a good reminder: the safest service is the one that never touches the public internet.
This guide shows how I isolate Postgres and Redis behind private networking, expose only the reverse proxy to the outside world, then connect instances cleanly across a private subnet or a zero-trust mesh.
If you are still planning the full setup the big picture lives in the self-host n8n on VPS guide.
If you are already running queue mode you may also want the details in n8n queue mode: scaling with Redis and workers and Scaling n8n with Redis.
What private networking means here
- Only the reverse proxy is public. n8n’s editor and webhooks sit behind Nginx or Caddy on port 443.
- Datastores live on private interfaces. Postgres and Redis bind to
127.0.0.1
or a private RFC1918 subnet. - Firewall denies by default. Allow
80/443
for the proxy,22
for SSH from a trusted IP, and nothing else. - Inter-VPS traffic runs over a private subnet or a mesh like WireGuard or Tailscale.
- Docker creates isolated networks. Services talk on user-defined networks with
internal: true
.
For a bigger security picture, see n8n security best practices.
Reference topology
- edge-1: reverse proxy, public, HTTPS only
- n8n-main: editor and webhook receiver, private network
- postgres-1: Postgres on private IP 10.10.0.10
- redis-1: Redis on private IP 10.10.0.11
- worker-*: n8n workers, private only
Docker Compose with internal networks
yaml
version: "3.9"
networks:
public:
driver: bridge
private:
driver: bridge
internal: true
volumes:
pgdata:
n8n_data:
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
depends_on:
- n8n-main
networks:
- public
- private
n8n-main:
image: n8nio/n8n
restart: unless-stopped
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- QUEUE_BULL_REDIS_HOST=redis
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=automation.example.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://automation.example.com/
- N8N_PORT=5678
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
- redis
networks:
- private
n8n-worker:
image: n8nio/n8n
restart: unless-stopped
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- QUEUE_BULL_REDIS_HOST=redis
- EXECUTIONS_PROCESS=queue
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
depends_on:
- postgres
- redis
deploy:
replicas: 2
networks:
- private
postgres:
image: postgres:15
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=n8n
command: ["postgres","-c","listen_addresses=127.0.0.1,postgres"]
volumes:
- pgdata:/var/lib/postgresql/data
networks:
private:
aliases: [ "postgres" ]
redis:
image: redis:7
restart: unless-stopped
command: ["redis-server","--bind","127.0.0.1,redis","--appendonly","yes","--requirepass","${REDIS_PASSWORD}"]
networks:
private:
aliases: [ "redis" ]
Notes: Postgres and Redis bind only to loopback or service name. The proxy touches both networks. The private network has internal: true
.
Firewall setup
Example with UFW:
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 443/tcp
ufw allow 80/tcp
ufw allow from 203.0.113.10 to any port 22 proto tcp
ufw enable
On database hosts, allow only the private subnet:
ufw allow from 10.10.0.0/24 to any port 5432 proto tcp
ufw allow from 10.10.0.0/24 to any port 6379 proto tcp
Validate with ss -tulpn
and nmap
from outside.
Postgres configuration
postgresql.conf
:
listen_addresses = '127.0.0.1,10.10.0.10'
pg_hba.conf
:
host n8n n8n 10.10.0.0/24 scram-sha-256
Restart Postgres and point n8n to the private IP. For database choices see PostgreSQL vs SQLite for n8n.
Redis configuration
redis.conf
minimal:
bind 127.0.0.1 10.10.0.11
protected-mode yes
requirepass strongpassword
appendonly yes
Check with:
ss -ltnp | grep -E '5432|6379'
Workers connect with:
QUEUE_BULL_REDIS_HOST=10.10.0.11
QUEUE_BULL_REDIS_PASSWORD=strongpassword
For scaling details, see n8n queue mode: scaling with Redis and workers.
Zero-trust mesh: WireGuard or Tailscale
If your VPS provider has no VPC, build your own.
WireGuard example:
Assign 10.13.0.0/24, configure peers, allow only required IPs. Bind Postgres to the WireGuard IP.
Tailscale:
Easier to operate, gives you stable private IPs and ACLs. Works fine for Postgres and Redis. Just use the Tailscale IPs in your n8n .env
.
Environment variables for private networking
N8N_HOST=automation.example.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://automation.example.com/
N8N_PORT=5678
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=10.10.0.10
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=secret
QUEUE_BULL_REDIS_HOST=10.10.0.11
QUEUE_BULL_REDIS_PASSWORD=secret
N8N_ENCRYPTION_KEY=32-byte-secret
If webhook URLs look wrong in the UI, check the reverse proxy article: Fix webhook URL issues in n8n behind a reverse proxy.
Monitoring and health checks
- Use Uptime Kuma to test webhooks.
- Track queue depth, execution duration and DB latency in Prometheus. Guide here: Monitor n8n with Prometheus and Grafana.
- Keep working backups. Recovery steps are in Backups and disaster recovery for n8n.
Migrating an existing public setup
- Map exposed services with
ss
anddocker ps
. - Add a private subnet or Tailscale.
- Move Postgres and Redis to private bind addresses.
- Adjust firewall, leaving only 80/443 public.
- Test from outside with
nmap
.
When you actually need public listeners
Only the reverse proxy should be public. Webhooks, editor access, SSL termination. Postgres and Redis must never be public.
If you want to explore adding AI integrations, see Integrating AI into n8n on a VPS.
FAQ
Should I split Postgres and Redis onto their own VPS?
For small installs you can keep them together. As load grows, split Postgres first, then Redis. Always keep them on the same private subnet or mesh.
Do I still need a firewall if services only bind to 127.0.0.1?
Yes. A firewall catches mistakes and enforces intent.
How do I reach Postgres if it has no public IP?
Use SSH port forwarding or a Tailscale/WireGuard connection. Do not expose port 5432.
What about queue mode, does it change anything?
No. Only n8n-main needs to be reachable by the proxy. Workers and datastores stay private.
How do I test that nothing is exposed?
From outside, run nmap -Pn your.server.ip -p 1-65535
. You should only see 80, 443 and 22 (if you left SSH public).