Wiring up Wireguard, Caddy, and Docker on a home server
I wanted to write a little guide, mostly for my own future use, to describe how I set up Wireguard on my home sever. Hopefully this will be of use to someone else!
Why Wireguard?
I run a home server (an old, reliable, much-battered HP Proliant called Ottoline). It runs a few public-facing websites in Docker containers, like a library of plants in our house, a gitea, and a Nextcloud sever. It also runs a bunch of services which I'd like to keep private, like a Glances system monitor and Portainer.
I have a domain name (let's call it ottoline.com) which I've linked up to my home sever via Namecheap's excellent dynamic DNS API. This means that I can access services on my home server via subdomains: https://plants.ottoline.com, https://git.ottoline.com, https://glances.ottoline.com. The subdomains are wired up via a Caddy Docker container - requests come in, are received by Caddy, and are proxied to the internal networks of my Docker containers.
I want to keep these two nice features the same:
- Accessing services via subdomain names
- Accessing services from outside my home network
But:
- I want to make some services to stay private and some to stay public.
Wireguard felt like the perfect solution - running Wireguard in a Docker container would allow me to set up a backdoor to my server, accessible only when I'm connected to the Wireguard VPN. Public services would always work, but private services would return a 403 error if the request to them came from outside my home network (and, by extension, my VPN).
Sounds simple! But how do I do it?
Setting up Caddy
I started by reworking my Caddyfile, Caddy's main configuration file.
For publicly accessible sites, the configuration remained the same:
plants.ottoline.com {
file_server
root * /usr/share/caddy
encode gzip
}
nextcloud.ottoline.com {
reverse_proxy nextcloud:80
}
For private sites, I used a named matcher to match only internal IPs (including Docker's 172.x.x.x
IPs):
portainer.ottoline.com {
@internal {
remote_ip 192.168.0.0/16 172.0.0.0/8
}
handle @internal {
reverse_proxy portainer:9000
reverse_proxy portainer:8000
}
respond 403
}
If the IP is in the @internal
range, it'll be reverse proxied. Otherwise, Caddy responds with a 403. Nice!
With this setup in place, I was already half the way there - I could access public sites from the wider Internet, and private sites only from inside my home network. Now for the VPN.
Setting up Wireguard
I added the following section to my docker-compose.yml
and ran docker-compose up -d
:
wireguard:
image: linuxserver/wireguard
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- GUID=1000
- TZ=Europe/London
- SERVERURL=wireguard.ottoline.com
- PEERS=2
- PEERDNS=auto
- INTERNAL_SUBNET=10.10.10.0
volumes:
- ./wireguard:/config
- /lib/modules:/lib/modules
ports:
- 51820:51820/udp
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
I set up two peers - one for my laptop, one for my phone. When this configuration is run, it will create two peer configuration files, peer1.conf
and peer2.conf
, inside the ./wireguard
directory on the host system. I copied the first one to /etc/wireguard/wg0.conf
on my laptop, but I changed the following lines:
[Interface]
...
DNS = 192.168.0.4 # My home server's IP address
[Peer]
...
AllowedIPs = 192.168.0.0/16
The AllowedIPs
directive means that when Wireguard is enabled on my laptop, it will only send requests to the local server (which includes the subdomain names I set up) through the VPN, and pass everything else outside the VPN. The DNS
directive means all DNS queries will go through the home server, which is essential because Caddy needs to know which requests are internal and which are external.
For my phone, I ran the following command on the server to display a QR code and scanned it with the excellently no-nonsense Wireguard app:
docker exec -it wireguard /app/show-peer 2
I then changed the same fields on the phone as I did on my laptop.
Setting up DNS
The final step in the process is setting up DNS on the home server to forward DNS queries being sent via Wireguard to the right Caddy routes. I used a lightweight service called dnsmasq for this. There seem to be a lot of dnsmasq settings one can set, but these worked for me.
First, I disabled the default systemd-resolved service:
systemctl mask systemd-resolved
systemctl stop systemd-resolved
systemctl disable systemd-resolved
Then I installed dnsmasq and edited /etc/dnsmasq.conf
:
domain-needed
bogus-priv
cache-size=1000
server=8.8.8.8
server=4.4.4.4
address=/.ottoline.com/192.168.0.4
This sets two default DNS servers (8.8.8.8 and 4.4.4.4) for unresolved queries, and catches all queries to ottoline.com and its subdomains to the home server's local IP.
Then, turn on dnsmasq:
systemctl enable dnsmasq
systemctl start dnsmasq
Finally, I needed to edit /etc/resolv.conf
(I'm not sure why, but considering the rest of this somehow worked, I'm not counting my blessings). I think that without it, DNS queries from inside the home server don't resolve). Because on Ubuntu, resolv.conf
is controlled by NetworkManager, it's just a symlink and is reset on reboots:
ls -la /etc/resolv.conf
lrwxrwxrwx 1 root root 32 Jan 11 10:47 /etc/resolv.conf -> /run/systemd/resolve/resolv.conf
I removed the symlink by renaming /etc/resolv.conf
to /etc/resolv.conf.bak
and creating a new, regular /etc/resolv.conf
, containing the following:
nameserver 8.8.8.8
nameserver 8.8.4.4
With this final piece in place, I am able to access some of my containers only via the VPN, and others anytime.