When you self-host at home, a stable fiber connection usually gives you everything you need: fast speeds, sometimes a public IPv4 address and minimal latency. But when the fiber drops - whether because of an outage, provider work, or hardware failure - your uptime goes with it. So does your household WiFi.
In my homelab setup, I serve dozens of DNS records, for example theawesomegarage.com and viking-dominion.app). My self-hosting strategy worked great for many years, but during the last three months I've experienced three fiber outages from different reasons. A ran over fiber post, carrier misconfiguration and one still from unknown reasons. I decided to finally get a permanent secondary internet connection, ready to take over if the primary one failed. I decided on a 5G LTE backup provided by a TP-Link NX500 router, just because it was available in a store near me when I was without Internet. Ideall I'd have bought a Teltonika RUTX50, but the TP-Link works surprisingly well for now.
Independently of the hardware used for the failover, the 5G LTE carrier put me behind CGNAT. So I didn't get a public IP address like I'm used to with the fiber connection. That meant port forwarding on my Ubiquiti UDM wouldn't cut it anymore, so I had to do something new.
This post explains how I solved it: by renting the tiniest VPS at Hetzner, tunneling traffic back to my home lab over WireGuard, and using Traefik as a public ingress on the VPS. The result: I can switch from fiber to 5G and still keep almost 100% uptime (at lower speed, but service continuity is more important).
I should add that I do not want to use out of the box tunneling services such as Tailscale Funnel or cloudflared. The amount of traffic I have would certainly have my Cloudflare account closed pretty quickly unless I signed up for some of the paid services, and Tailscale would also not be free, so I decided to build my own solution.
Due to privacy concerns, I chose Hetzner because it's European, and to top it, I chose a Finnish location. The pricing is really good and Hetzner has a decent reputation.
HostSNI(...)
passthrough on port 443 → tunnel back home (TLS termination stays at home).helloworld.theawesomegarage.com
), Traefik terminates TLS directly on the VPS and serves a Docker container locally.Hetzner also provides a built-in firewall and other security tools if needed.
A central component to make this work is a persistent WireGuard tunnel between the VPS (Hetzner) and the home server. All traffic Traefik needs to forward is sent through this tunnel.
On both VPS and home:
sudo apt update
sudo apt install wireguard -y
On each side (run separately):
wg genkey | tee privatekey | wg pubkey > publickey
You'll now have privatekey
and publickey
for both sides.
vps-private-key
, vps-public-key
home-private-key
, home-public-key
Create /etc/wireguard/wg0.conf
:
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <vps-private-key>
[Peer]
PublicKey = <home-public-key>
AllowedIPs = 10.10.0.2/32
Create /etc/wireguard/wg0.conf
:
[Interface]
Address = 10.10.0.2/24
PrivateKey = <home-private-key>
[Peer]
PublicKey = <vps-public-key>
Endpoint = <vps-public-ip>:51820
AllowedIPs = 10.10.0.1/32
PersistentKeepalive = 25
Note: PersistentKeepalive
is critical on the home side if it's behind NAT/CGNAT. It keeps the tunnel open by sending a packet every 25s.
On both VPS and home:
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
Check status:
sudo wg show
The tunnel will automatically spin up on server boot. If it fails, check that you allow outgoing UDP on port 51820 to flow from your home lab to the VPS, and open for incoming traffic on UDP port 51820 in your VPS firewall.
With WireGuard up:
10.10.0.2
10.10.0.1
In dynamic.yml
(a Traefik config we'll come back to later), services use those IPs:
services:
home80:
loadBalancer:
servers:
- url: "http://10.10.0.2:80"
home443:
loadBalancer:
servers:
- address: "10.10.0.2:443"
That way, Traefik on the VPS doesn't care whether home is on fiber or LTE - it always routes through WireGuard.
This downloads Traefik as a binary from Traefik themselves. You could as well run it as a Docker container with docker-compose, as outlined in an article I wrote back in 2024, but then you'd have to adjust the rest of the steps below.
curl -sL https://github.com/traefik/traefik/releases/download/v3.5.2/traefik_v3.5.2_linux_amd64.tar.gz | sudo tar -xz -C /usr/local/bin traefik
sudo useradd -r -d /var/lib/traefik -s /usr/sbin/nologin traefik
sudo mkdir -p /etc/traefik /var/lib/traefik
sudo chown -R traefik:traefik /etc/traefik /var/lib/traefik
/etc/systemd/system/traefik.service
:Make Traefik start on boot, give permissions to bind to reserved ports and define traefik user.
[Unit]
Description=Traefik
After=network.target
[Service]
User=traefik
Group=traefik
ExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.yml
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
Restart=always
Environment=CF_DNS_API_TOKEN=***
[Install]
WantedBy=multi-user.target
Note, that the Environment variable we add here is a necessary token to run DNS-based certificate management through plugins for various DNS providers that are compatible with Lego. If you use something else than CloudFlare, for example Domeneshop for dnsChallenge, check out the Lego link to find the tokens you need to provide.
/etc/traefik/traefik.yml
:Static config used by the routers and services you set up in the dynamic configuration later.
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
file:
filename: /etc/traefik/dynamic.yml
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
log: { level: DEBUG }
accessLog: {}
certificatesResolvers:
zerosslCloudflare:
acme:
caServer: https://acme.zerossl.com/v2/DV90
email: [email protected]
storage: /var/lib/traefik/acme-zerossl-cloudflare.json
dnsChallenge:
provider: cloudflare
eab:
kid: ***
hmacEncoded: ***
Note, you need to refer to [ZeroSSL documentation](https://zerossl.com/documentation/acme/generate-eab-credentials/) to get the kid and hmacEncoded values. This is necessary because ZeroSSL is a paid service, and you need to authenticate to generate certs. It would be simple to use Let's Encrypt instead. Refer to [Traefik docs](https://doc.traefik.io/traefik/reference/install-configuration/tls/certificate-resolvers/acme/) for a basic Acme web resolution example (http challenge).
Also note that here, we tell Traefik to use the Docker as a provider (allows us to use Docker labels to configure Traefik), so we need to give Traefik access to Docker.
# Add traefik user to the docker group
sudo usermod -aG docker traefik
# Apply group membership by restarting traefik
sudo systemctl restart traefik
If you have no intentions of running local services on your VPS, drop docker as a provider and don't add traefik to the docker group.
/etc/traefik/dynamic.yml
:This file tells Traefik on the VPS what to do with incoming requests. Specifically, it decides whether traffic should be served locally on the VPS or forwarded ("tunneled") back to the home server over WireGuard.
In short: this config makes the VPS act like a smart relay - only terminating TLS for domains you explicitly define elsewhere, and sending all the rest through to your home lab unchanged.
tcp:
routers:
to-home:
entryPoints: ["websecure"]
rule: HostSNI(`remim.com`) || HostSNI(`www.remim.com`) || HostSNI(`theawesomegarage.com`) || HostSNI(`skillia.app`) # etc.
service: home443
tls:
passthrough: true
services:
home443:
loadBalancer:
servers:
- address: "10.10.0.2:443"
http:
routers:
to-home-80:
entryPoints: ["web"]
rule: "HostRegexp(`{host:.+}`)"
service: home80
priority: 1
services:
home80:
loadBalancer:
servers:
- url: "http://10.10.0.2:80"
docker-compose.yml
:A warning! The hello world container in this chapter is a sidetrack in this guide. You don't need it for the purpose of the guide, but consider it a bonus example of what you can do if you do cash out on a VPS.
If a request isn't caught by the dynamic.yml definition, your Traefik will not serve a valid page. Unless you set up a new site to be served locally by for example running docker containers that are labelled in such a way that the VPS Traefik can understand it. Using labels on docker containers to configure Traefik is possible because we added docker as a provider for Traefik in traefik.yml.
This example sets up a helloworld site that runs a simple server that only serves out the page helloworld.html. The helloworld.html page is read relatively from the docker-compose.yml location, so you almost certainly want to organize your volumes better than this example shows.
services:
helloworld:
image: nginx:alpine
container_name: helloworld
restart: unless-stopped
volumes:
- ./helloworld.html:/usr/share/nginx/html/index.html:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.helloworld.entrypoints=websecure"
- "traefik.http.routers.helloworld.rule=Host(`helloworld.theawesomegarage.com`)"
- "traefik.http.routers.helloworld.tls.certresolver=zerosslCloudflare"
This way, your VPS can double up as a secondary web-server off-site. If you want your services to auto-deploy when you release new versions, check out my guide on building and deploying locally using GitHub actions and webhooks. Normally you wouldn't want to create a runner on your VPS, so just stick to adding webhooks for deploying new images!
You could also set up additional local services by adding http routers and loadbalancer services in dynamic.yml directly, instead of using docker at all, but refer to the Traefik docs for this.
When the VPS is set up and Wireguard connected, you really don't have to do much on the home server.
10.10.0.1
(VPS) reaches 10.10.0.2
(home).This hybrid model enables you to self host without a public IP, and as an added bonus, gives you a 5G LTE fallback out of the box. This works pretty good with the Unifi UDM, which unfortunately lacks this fallback capability by itself.
22/tcp
, 80/tcp
, 443/tcp
, 51820/udp
.