Local hosting behind a VPS
I want to replicate the architecture used by autistici/inventati, which hosts services on machines that are not directly accessible from the internet, and external traffic is routed to them from a VPS using a VPN.
The architecture includes:
- a wireguard VPN container on the VPS
- a caddy container on the VPS, that shares the network with the wireguard container
- a wireguard VPN container on the home server, that connects to the wireguard container on the VPS
- a web app running on the home server inside a container, sharing the network with the wireguard container on the home server
The wireguard container on the VPS looks like:
services:
vpn:
image: linuxserver/wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
restart: unless-stopped
sysctls:
net.ipv4.ip_forward: 1
ports:
- "80:80"
- "443:443"
- "51820:51820/udp"
volumes:
- ./wg-config/wg0.conf:/config/wg_confs/wg0.conf
environment:
- PUID=1000
- PGID=1000
The wg0.conf file has the following contents:
[Interface]
Address = 10.0.0.1/24
PrivateKey = SERVER_PRIVATE_KEY
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
On the home server, the container has the following contents:
services:
vpn:
image: linuxserver/wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
restart: unless-stopped
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- ./wg-config/wg0.conf:/config/wg_confs/wg0.conf
- /lib/modules:/lib/modules
healthcheck:
test: ["CMD-SHELL", "ping -c 1 -w 5 10.0.0.1 || exit 1"]
interval: 30s
The wg0.conf file has the following contents:
[Interface]
Address = 10.0.0.2/24
PrivateKey = APP_PRIVATE_KEY
PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = 1.2.3.4:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Give a different IP address to each service that you add.
The container for the app running on the home server needs to be configured with network_mode: "service:vpn".
The private keys for the two parties have to be generated with:
wg genkey
The public keys can then be generated with:
echo PRIVATE_KEY | wg pubkey
Adding apps to the VPS
Each app must be added to the configuration of wireguard container running on the VPS:
[Peer]
PublicKey = APP_PUBLIC_KEY
AllowedIPs = 10.0.0.2/32
To add a new peer to the VPS without restarting the wireguard container you can use:
docker compose exec vpn wg set wg0 peer APP_PUBLIC_KEY allowed-ips 10.0.0.2/32
The reverse proxy on the VPS looks like this:
services:
reverse-proxy:
image: caddy:latest
restart: unless-stopped
network_mode: "service:vpn"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/data:/data
- ./caddy/config:/config
The Caddyfile configuration should contain an entry for each app that you want to expose:
website.example.com {
reverse_proxy 10.0.0.2:80
}
If your app needs to receive traffic on ports other than 80 and 443, you need to:
- expose the port on the wireguard container running on the VPS
- route traffic directly from the wireguard container to the app container by updating the wireguard configuration
PostUp = iptables -t nat -A PREROUTING -p tcp --dport 22 -j DNAT --to-destination 10.0.0.2:22
PostDown = iptables -t nat -D PREROUTING -p tcp --dport 22 -j DNAT --to-destination 10.0.0.2:22
Open questions:
- How can I add/edit/remove a single service from this setup without affecting other services? Restarting the wireguard container on the VPS prevents all services from working
- How can I mark a single service as in “maintenance mode” with Caddy?
- How can we simplify this configuration to avoid repetitions in the VPS and home server?