Route traffic through a WireGuard tunnel to your home network with automatic HTTPS. Restrict private services to your home IP with a single reusable snippet — no cert management required.
This setup uses Caddy on your VPS as the public-facing SSL gateway. Caddy handles automatic certificate issuance and renewal via Let's Encrypt, then proxies all traffic through a WireGuard tunnel to your home network where Nginx Proxy Manager (or any other reverse proxy) routes requests to individual Docker containers.
Public services are accessible to anyone. Private services abort connections from any IP that isn't your home — controlled entirely by a single reusable Caddy snippet.
subdomain.yourdomain.com (your VPS IP)certbot cron job, no manual cert paths, and no SSL block to write. You specify your domain — Caddy handles the rest.
Before starting, make sure you have the following in place:
80 and 443 open in UFW: sudo ufw allow 80 && sudo ufw allow 443Caddy is not in the default Ubuntu repos, so we add the official Caddy package repository first.
# Install required packages
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# Add Caddy's GPG key
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# Add Caddy repository
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
sudo tee /etc/apt/sources.list.d/caddy-stable.list
# Update and install
sudo apt update && sudo apt install -y caddy
Verify the installation and check that the service is running:
caddy version
sudo systemctl status caddy
Since Caddy handles all routing at the application layer, your WireGuard config on the VPS
needs to handle only tunnel routing — no DNAT port forwarding rules are needed.
If you have existing port forwarding rules in your PostUp/PostDown blocks, remove them.
[Interface]
Address = 10.0.0.1/24
PrivateKey = YOUR_VPS_PRIVATE_KEY
ListenPort = 51820
# NAT masquerading only — no DNAT port forwards needed
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
# Your home VM / NPM server
PublicKey = YOUR_HOME_VM_PUBLIC_KEY
AllowedIPs = 10.0.0.2/32
PREROUTING DNAT rules to this WireGuard config. Caddy proxies at Layer 7 — iptables DNAT rules will conflict and cause routing issues.
Fill in your details and add your subdomains below. The Caddyfile updates live as you type. Check Public to allow access from anywhere, or leave unchecked to restrict to your home IP only.
Fill in all fields — the config below updates automatically
# Fill in the fields above to generate your Caddyfile
Copy the generated Caddyfile from above and write it to /etc/caddy/Caddyfile
on your VPS, then validate and reload.
# Open the Caddyfile and paste the generated config
sudo nano /etc/caddy/Caddyfile
After saving, validate the syntax and reload:
# Validate config for syntax errors
caddy validate --config /etc/caddy/Caddyfile
# Format the Caddyfile (optional but recommended)
caddy fmt --overwrite /etc/caddy/Caddyfile
# Reload Caddy — applies changes without dropping connections
sudo systemctl reload caddy
Check the service status and follow logs to confirm a clean start:
sudo systemctl status caddy
sudo journalctl -u caddy -f
systemctl reload, not restartReload applies the new config gracefully without dropping active connections. Use restart only if reload itself fails.
Since Caddy now handles SSL termination on the VPS, your NPM proxy hosts need to be updated. NPM no longer needs to manage certificates or enforce access rules — it simply routes plain HTTP from the WireGuard tunnel to your containers.
http — SSL is terminated at Caddy, not NPMWith a peer connected, run these checks to confirm everything is working end-to-end.
Test a public service from anywhere:
curl -I https://public.yourdomain.com
# Expect: HTTP/2 200
Verify a private service blocks outside access:
curl -I https://private.yourdomain.com
# Expect: connection closed — Caddy aborts non-home IPs
Verify SSL certificate details:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -dates
Confirm the WireGuard tunnel and peer reachability:
sudo wg show
# latest-handshakes should show a recent timestamp
ping -c 3 10.0.0.2
# Must succeed for Caddy to reach NPM
New private service:
sudo systemctl reload caddyhome_only snippet does the heavy liftingEvery block that uses import home_only is automatically protected. Adding new private services is just a matter of adding one subdomain block — no extra IP rules needed.
New public service:
/etc/caddy/Caddyfilesudo systemctl reload caddyCaddy fails to start or reload:
caddy validate --config /etc/caddy/Caddyfile
sudo journalctl -u caddy -n 50 --no-pager
dig subdomain.yourdomain.com to verifysudo ufw statusPrivate service accessible from outside:
import home_only is present in that subdomain blockcurl ifconfig.me from home)sudo systemctl reload caddy after any Caddyfile changeConnection refused on a proxied service:
# 1. Is the WireGuard tunnel up?
sudo wg show
# 2. Can the VPS reach the home tunnel IP?
ping -c 3 10.0.0.2
# 3. Can Caddy reach NPM on the home side?
curl -v http://10.0.0.2:PORT
# 4. Follow Caddy logs while making a test request
sudo journalctl -u caddy -f
sudo journalctl -u caddy -f in one terminal while making test requests in another. Caddy logs each request with the upstream it tried to reach, making it easy to trace exactly where a failure occurs.