This guide covers connecting one or more home servers (Raspberry Pi, NUC, old laptop, etc.) to your exit node so their services are publicly accessible.
- Each device gets its own WireGuard VPN client with a unique VPN IP (e.g.
10.13.13.2,10.13.13.3). - Multiple services on one device share the VPN connection by using
network_mode: "service:wireguard-client"— each listens on a different port. - Caddy on the exit node routes
domain → VPN-IP:port.
Pi 1 (10.13.13.2) Pi 2 (10.13.13.3)
├── webapp :8080 ├── blog :4000
├── api :3000 └── dashboard :9090
│
Caddy routes:
app.example.com → 10.13.13.2:8080
api.example.com → 10.13.13.2:3000
blog.example.com → 10.13.13.3:4000
dashboard.example.com → 10.13.13.3:9090
On your client machine you need:
- Docker and Docker Compose installed
- Internet access (no public IP required — that's the whole point!)
-
Open the WireGuard web UI on your exit node at port
51821.Security note: The web UI should not be exposed to the public internet over plain HTTP. Use an SSH tunnel to access it securely:
ssh -L 51821:localhost:51821 user@your-vps-ip
Then open
http://localhost:51821in your browser. -
Log in with the password you set in
WG_PASSWORD. -
Click "New Client" and give it a name (e.g.
pi-1). -
Download the generated
.conffile. -
Note the VPN IP assigned (e.g.
10.13.13.2) — you'll need it for Caddy.
Copy the client/ folder from this repo to your client machine.
scp -r client/ pi@raspberrypi:~/exit-client/On the client machine:
cd ~/exit-client
mkdir -p wg-config
# Copy the .conf file from step 1
cp /path/to/pi-1.conf wg-config/wg0.conf
# Start the VPN connection
docker compose up -dVerify the tunnel is up:
docker exec wireguard-client wg showYou should see a handshake timestamp and data transfer stats.
If you prefer to run WireGuard natively:
# Install WireGuard
sudo apt install wireguard
# Copy the config
sudo cp /path/to/raspberry-pi.conf /etc/wireguard/wg0.conf
# Start the tunnel
sudo wg-quick up wg0
# Enable on boot
sudo systemctl enable wg-quick@wg0The easiest approach is to add your app to client/docker-compose.yml using network_mode: "service:wireguard-client". This makes your app share the VPN network stack, so it's directly reachable at the VPN IP.
services:
wireguard-client:
# ... (already configured)
myapp:
image: nginx:alpine
container_name: myapp
restart: unless-stopped
network_mode: "service:wireguard-client"
# No ports needed — traffic arrives via the VPN tunnelYour app is now reachable at 10.13.13.2:80 from the exit node.
If your app runs in a different Compose project, you can either:
A) Use host networking on the WireGuard client and access the app via localhost:
# In client/docker-compose.yml
services:
wireguard-client:
# ... existing config ...
network_mode: hostThen your app running on any port on the host is reachable via the VPN IP.
B) Use Docker networks to connect your app to the WireGuard client's network.
Back on your VPS, edit caddy/Caddyfile to route traffic to the client:
myapp.example.com {
reverse_proxy 10.13.13.2:80
}Make sure you have a DNS A record pointing myapp.example.com to your VPS IP.
Reload Caddy:
docker compose exec caddy caddy reload --config /etc/caddy/CaddyfileCaddy will automatically obtain a TLS certificate from Let's Encrypt. Your app is now live at https://myapp.example.com.
Every service that uses network_mode: "service:wireguard-client" shares the VPN tunnel and is reachable at the device's VPN IP. Just make sure each service listens on a unique port.
# client/docker-compose.yml
services:
wireguard-client:
# ... (VPN tunnel — already configured)
webapp:
image: my-web-app:latest
restart: unless-stopped
network_mode: "service:wireguard-client"
environment:
- PORT=8080
# Reachable at 10.13.13.2:8080
api:
image: my-api:latest
restart: unless-stopped
network_mode: "service:wireguard-client"
environment:
- PORT=3000
# Reachable at 10.13.13.2:3000
dashboard:
image: my-dashboard:latest
restart: unless-stopped
network_mode: "service:wireguard-client"
environment:
- PORT=9090
# Reachable at 10.13.13.2:9090Then in caddy/Caddyfile on the exit node:
app.example.com {
reverse_proxy 10.13.13.2:8080
}
api.example.com {
reverse_proxy 10.13.13.2:3000
}
dashboard.example.com {
reverse_proxy 10.13.13.2:9090
}Important: Your app must listen on
0.0.0.0(all interfaces), not127.0.0.1. Most Docker images do this by default.
Need multiple exit nodes instead? If you want the same services reachable from more than one VPS (for redundancy or geographic distribution), see Multiple Exit Nodes.
Each device gets its own WireGuard client. Repeat the setup for each device:
- Open the wg-easy UI and create a new client (e.g.
pi-1,pi-2). - Download each
.conffile. - Note each device's VPN IP.
Copy the client/ folder and set up its own WireGuard config:
# Device 1 (Raspberry Pi 1)
cd ~/exit-client
mkdir -p wg-config
cp /path/to/pi-1.conf wg-config/wg0.conf
# Edit docker-compose.yml to add your services
docker compose up -d# Device 2 (Raspberry Pi 2)
cd ~/exit-client
mkdir -p wg-config
cp /path/to/pi-2.conf wg-config/wg0.conf
# Edit docker-compose.yml to add your services
docker compose up -d# Pi 1 services (VPN IP: 10.13.13.2)
app.example.com {
reverse_proxy 10.13.13.2:8080
}
api.example.com {
reverse_proxy 10.13.13.2:3000
}
# Pi 2 services (VPN IP: 10.13.13.3)
blog.example.com {
reverse_proxy 10.13.13.3:4000
}
monitoring.example.com {
reverse_proxy 10.13.13.3:9090
}Reload Caddy after any changes:
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile# Check WireGuard status
docker exec wireguard-client wg show
# Check logs
docker logs wireguard-client- Make sure UDP port 51820 is open on the VPS firewall.
- Make sure the
.conffile is correctly placed inwg-config/wg0.conf.
# From the VPS, test connectivity to the client VPN IP
docker exec caddy ping 10.13.13.2
# Check if the app is listening
docker exec wireguard-client curl -s http://localhost:8080- Make sure your app is using
network_mode: "service:wireguard-client". - Make sure the app is listening on
0.0.0.0, not127.0.0.1.
- Check that DNS for your domain points to the VPS public IP.
- Check Caddy logs:
docker logs caddy - Make sure ports 80 and 443 are open and not used by another service.