7.0 KiB
VPN Setup for Baileys API (Gluetun + Mullvad)
Problem
Hosting providers like Hostinger have their IP ranges mass-blocked by Meta. This causes WhatsApp connections through Baileys to fail. The solution is to route only Baileys traffic through a VPN, keeping everything else (Rails, Sidekiq, Postgres, Redis) on the regular network.
Architecture
┌─────────────────────────────────────────────────────┐
│ Docker Network (coolify) │
│ │
│ ┌──────────┐ ┌──────────────────────────────┐ │
│ │ Rails │ │ Gluetun (VPN tunnel) │ │
│ │ Sidekiq │───▶│ :3025 ──▶ Baileys API │ │
│ └──────────┘ │ (network_mode: service) │ │
│ │ │ │ │ │
│ │ │ │ VPN (WireGuard) │ │
│ │ │ ▼ │ │
│ │ │ Mullvad SP (Brazil) │ │
│ │ └──────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Redis │◀───────│ Baileys │ │
│ │ Postgres│ │ (via VPN)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
Gluetun creates a WireGuard VPN tunnel. Baileys shares Gluetun's network via network_mode: "service:gluetun", so all WhatsApp traffic exits through the VPN IP. Internal Docker traffic (Redis, etc.) is exempted via firewall subnet rules.
Step 1 — Get Mullvad Credentials
- Go to https://mullvad.net/en/account/create and generate an account (no email needed — save the 16-digit account number).
- Add credit (€5/month, accepts card, PayPal, crypto).
- Go to https://mullvad.net/en/account/wireguard-config, log in, select Linux, click Generate key, choose Brazil > São Paulo, and download the
.conffile. - From the downloaded file, note:
PrivateKey— e.g.KLaIt4oAaI6Iz4iQhSS9/0UBlbvfmG1LC/NWGXW/DH4=Address— use only the IPv4 address, e.g.10.67.152.85/32(discard the IPv6 address)
Important: Gluetun does not support IPv6. Only use the IPv4 address from the
.conffile.
Step 2 — Docker Compose Changes
2.1 Add the gluetun service (baileys-api compose)
gluetun:
image: qmcgaw/gluetun
restart: always
cap_add:
- NET_ADMIN
ports:
- '3025:3025'
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=<your-private-key>
- WIREGUARD_ADDRESSES=<your-ipv4-address>/32
- SERVER_COUNTRIES=Brazil
- SERVER_CITIES=Sao Paulo
- FIREWALL_OUTBOUND_SUBNETS=172.16.0.0/12,10.0.0.0/8,192.168.0.0/16
- DNS_KEEP_NAMESERVER=on
healthcheck:
test:
- CMD-SHELL
- 'wget -qO- https://ipinfo.io/ip'
interval: 30s
timeout: 10s
retries: 5
networks:
- coolify
Key environment variables:
| Variable | Purpose |
|---|---|
FIREWALL_OUTBOUND_SUBNETS |
Allows internal Docker traffic (Redis, etc.) to bypass the VPN. Must cover all private subnets used by Docker. |
DNS_KEEP_NAMESERVER |
Keeps Docker's internal DNS so containers can resolve hostnames like redis. Without this, you get getaddrinfo ENOTFOUND errors. |
Do NOT set
OWNED_ONLY=yes— Mullvad does not have owned servers in São Paulo, only rented ones. This filter would match zero servers.
2.2 Modify the baileys-api service
Apply these changes to the existing baileys-api service:
baileys-api:
# ... existing config ...
network_mode: 'service:gluetun' # Route all traffic through Gluetun
depends_on:
gluetun:
condition: service_healthy # Wait for VPN to be up
# REMOVE any 'ports' section — port 3025 is now exposed by gluetun
# REMOVE any 'networks' section — network_mode is incompatible with networks
condition: service_healthyis critical. Without it, baileys-api starts before the VPN tunnel is established, causing Redis connection timeouts.
2.3 Update BAILEYS_PROVIDER_DEFAULT_URL in Chatwoot
In the Rails and Sidekiq services (or Coolify environment variables), change:
# Before
BAILEYS_PROVIDER_DEFAULT_URL=http://baileys-api:3025
# After
BAILEYS_PROVIDER_DEFAULT_URL=http://gluetun:3025
Since baileys-api now shares Gluetun's network, external services must address it via gluetun hostname.
2.4 Declare the shared network
If the baileys-api compose is a separate stack, declare the shared network:
networks:
coolify:
external: true
name: coolify
Step 3 — Verify
After deploying, run from the server's SSH terminal:
# Check container health
docker ps | grep -E "gluetun|baileys"
# Get the VPN exit IP (replace with your actual gluetun container name)
docker exec <GLUETUN_CONTAINER> wget -qO- https://ipinfo.io/ip
# Compare with the server's real IP
curl -s https://ipinfo.io/ip
If the two IPs are different, the VPN is working correctly.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Redis client error / connection timeout on Redis |
Baileys starts before VPN is ready | Add depends_on with condition: service_healthy |
Redis client error / connection refused |
Internal Docker traffic blocked by VPN firewall | Add 192.168.0.0/16 to FIREWALL_OUTBOUND_SUBNETS |
getaddrinfo ENOTFOUND redis |
Docker DNS not working inside VPN | Set DNS_KEEP_NAMESERVER=on in gluetun |
no server found: ... city sao paulo; owned servers only |
Mullvad has no owned servers in São Paulo | Remove OWNED_ONLY=yes from gluetun env |
interface address is IPv6 but IPv6 is not supported |
IPv6 address in WIREGUARD_ADDRESSES |
Use only the IPv4 address (remove the fc00:... part) |
REDIS_URL with hardcoded IP (e.g. 172.19.0.2) |
Docker internal IPs change on restart | Always use hostnames (e.g. redis://redis:6379) |
VPN Expiration
If the Mullvad subscription expires:
- WhatsApp stops working (Baileys can't connect through the VPN).
- Everything else keeps running normally (Rails, Sidekiq, Redis, Postgres are not affected).
- To restore: renew Mullvad, or revert the docker-compose changes to bypass the VPN entirely.