Having a home server that works at home is one thing. Having it work exactly the same when you’re out, on mobile data, at a café, anywhere, is what makes it actually useful day to day.
Getting this right was worth the effort.
Keeping exposure minimal#
The obvious shortcut for remote access is port forwarding: expose your services on port 443, put a login screen in front of them, done. The problem is that everything becomes internet-facing. Every service needs to be hardened. Every misconfiguration is a risk. And some things, like local DNS filtering, don’t make sense to expose publicly at all.
I ran into this before even setting up WireGuard. I’d tried to get always-on ad blocking outside the home by exposing AdGuard’s DNS-over-HTTPS endpoint publicly, so I could set it as Android’s Private DNS on mobile data. I set it up, then pulled it back. A publicly accessible DNS server and another open port on the router felt like more exposure than I wanted for something optional.
WireGuard is a cleaner approach. Your server stays completely private. The only port open on my router is 51820 UDP for WireGuard. Nothing else. Everything else goes through the tunnel — and the AdGuard DNS problem solved itself, since all VPN traffic routes through the home network anyway.
WireGuard#
WireGuard is a modern VPN protocol, faster and lighter than older options like OpenVPN. The battery impact on Android is noticeably lower, which matters if the VPN is running all the time.
I run it using a Docker container called wg-easy, which wraps WireGuard with a web UI for adding and managing clients. Without something like wg-easy, WireGuard configuration involves generating key pairs by hand and editing config files carefully. wg-easy turns it into a few clicks.
The core of the setup:
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy
environment:
- WG_HOST=yourdomain.com
- WG_DEFAULT_DNS=<your server local IP>
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
cap_add:
- NET_ADMIN
- SYS_MODULEWG_HOST is your public domain, what clients will connect to. WG_DEFAULT_DNS pushes your server’s AdGuard as the DNS resolver for all connected clients. This is the key detail that makes everything work seamlessly away from home: your phone uses AdGuard as DNS over the VPN, so photos.yourdomain.com still resolves to your local server, and the ad blocking still applies.
The web UI for managing clients runs on port 51821, accessible via a subdomain, internal only.
Set up DDNS at the same time#
One thing that bit me early: home internet connections have dynamic IPs that change occasionally. When mine changed, the VPN endpoint stopped working because the domain was still pointing to the old IP.
The fix is DDNS Updater, a small Docker container that watches your public IP and updates your Cloudflare DNS record whenever it changes. Set it up at the same time as WireGuard, not after you’ve been caught out by a stale IP.
On the phone: automatic tunneling#
On Android I use an app called WG Tunnel rather than the official WireGuard client. The reason is one specific feature: automatic tunneling based on WiFi network.
I set my home WiFi as trusted. When I’m home, the VPN is off. I’m already on the local network. The moment I join any other network, mobile data, a café, anywhere else, the VPN connects automatically without me touching anything.
The first time I walked out of the house and photos.yourdomain.com just loaded on mobile data without me doing anything, that was the setup paying off. It just runs in the background. I don’t think about it.
Fallback: Tailscale#
I also have Tailscale installed, mostly as insurance. Tailscale is particularly useful if your home internet is behind CGNAT, where you can’t open ports at all — WireGuard wouldn’t work in that scenario but Tailscale would. That’s not my situation, but it’s good to have a fallback while I’m still building confidence in the WireGuard setup. I’ll probably drop it once I’m satisfied everything holds up.
What went wrong: the speed problem#
WireGuard connected fine from day one. But speeds outside the house were noticeably slower than they should have been, around 40 Mbps when my connection was capable of 80+. It worked, but it felt off.
Turns out it was several things stacked on top of each other.
wg-easy defaulting to the wrong network interface. The container hardcodes eth0 for its firewall rules, but Ubuntu had named the ethernet adapter something different (eno1 in my case). The MASQUERADE rule that actually routes VPN traffic to the internet was being applied to an interface that didn’t exist. WireGuard was connecting, but traffic wasn’t routing properly. The fix was switching wg-easy to network_mode: host and setting the iptables rules manually with the correct interface name via WG_POST_UP and WG_POST_DOWN.
iptables legacy vs nft mismatch. Ubuntu 24.04 uses iptables-nft by default, but wg-easy writes its rules to iptables-legacy. The kernel was ignoring them entirely. Writing the rules explicitly in the compose file worked around this.
The server was on WiFi. After all that, speeds were still not where they should be. The laptop was running wirelessly, which means all VPN traffic in and out was competing on the same radio. WiFi is half-duplex. I plugged in an ethernet cable and speeds jumped to 130 Mbps.
None of these were obvious from the outside. WireGuard appeared to be working the whole time. The only sign something was wrong was the speed. If your VPN feels sluggier than expected, check the interface name and iptables backend first — and make sure your server is actually on ethernet.
How it actually feels#
I leave the house, my phone connects to WireGuard automatically, and photos.yourdomain.com works exactly the same as when I’m home. Ad blocking applies. Everything is HTTPS. Nothing feels different.
That’s the goal. When the infrastructure is working well, you stop noticing it.
