Part of my current work — mentioned on the /now page — involves keeping a PostgreSQL read replica close to customers in Germany while the primary stays in Finland. The homelab equivalent of that problem is smaller but structurally identical: two sites, neither with a reliable static IP, that need to talk to each other as if they were on the same LAN.
Why not just use a static IP
The honest answer is cost and inertia. A static IP from either residential ISP involves a business-tier contract neither location needs for anything else. WireGuard with a keepalive and a lightweight rendezvous point solves the same problem for the price of a €4/month VPS, and it's the same tool I'd reach for professionally, so the practice isn't wasted.
The topology
Three peers: tre (Tampere, home network), hel (a relative's place in Helsinki, hosting a backup target), and relay (a small VPS with a real static IP, used only as a rendezvous point when both home connections have rotated their public IP at the same time).
WireGuard doesn't need a relay for two peers that can reach each other directly. I added one specifically because both ends are behind consumer ISPs with periodic IP changes, and having one stable point simplifies the config on both sides.
The WireGuard config, both ends
/etc/wireguard/wg0.conf — tre
[Interface]
Address = 10.10.0.1/24
PrivateKey = <tre-private-key>
ListenPort = 51820
[Peer] # hel
PublicKey = <hel-public-key>
AllowedIPs = 10.10.0.2/32, 192.168.20.0/24
PersistentKeepalive = 25
/etc/wireguard/wg0.conf — hel
[Interface]
Address = 10.10.0.2/24
PrivateKey = <hel-private-key>
ListenPort = 51820
[Peer] # tre
PublicKey = <tre-public-key>
AllowedIPs = 10.10.0.1/32, 192.168.10.0/24
PersistentKeepalive = 25
Keeping the tunnel up behind CGNAT
PersistentKeepalive = 25 is the setting that actually matters here — without it, the NAT mapping on either consumer router expires after a few minutes of silence, and the tunnel needs a new handshake before traffic flows again. Twenty-five seconds is short enough to keep most home routers' NAT tables from expiring the mapping, at the cost of a trivial amount of keepalive traffic.
If either side is actually behind carrier-grade NAT (common on some mobile and fiber ISPs), even keepalive won't guarantee reachability — that's what the relay peer is for. Route through it as a fallback rather than depending on direct peer-to-peer reachability.
Routing only what needs to cross
The AllowedIPs lines are deliberately scoped to each site's backup subnet rather than 0.0.0.0/0 — this is a link for one backup job and a couple of monitoring checks, not a full site-to-site VPN carrying general traffic. Keeping the scope narrow means a compromised device on either LAN can't casually reach the other network, which matters more once a residential connection is doing double duty as infrastructure.
The whole thing has been running for five months with two unplanned drops, both resolved automatically by the relay fallback within a minute. For a link built out of two home internet connections, that's a better track record than I expected going in.