NAME
Distributed Traefik with Tether
DATE
TAGS
My homelab has one public IP address. Port 443 forwards to one machine, and that is where the routing stops.
The containers are spread across different Proxmox nodes, a Raspberry Pi, and another server, so something central has to catch all the traffic and route it to the right internal IPs. Traefik is the obvious pick for that, but it has a constraint: it only supports a single HTTP provider endpoint. I cannot have every server push its own config to Traefik.
The usual options are one big static config file, or service discovery like Consul or Redis. I did not want to maintain either. What I wanted was something fast, stateless (no SQLite, no databases, no user accounts), and low maintenance.
The setup I landed on splits into two small pieces.
How it works
tether: sits next to the central Traefik instance. It is the single HTTP provider endpoint Traefik reads from, and it merges incoming configurations into one routing table.tetherd: a daemon that runs on the worker servers and watches the local Docker socket for standard Traefik labels.
Event-driven, not polling
My own previous iterations polled, and the older solutions I looked at did too. Polling means constant churn and stale state. The agents here are event-driven instead.
An agent detects the host IP, parses the labels, and pushes an update over the WebSocket only when a relevant port is exposed or changed. When a container starts on a worker node, the route goes to the central tether server, Traefik picks it up, and the UI updates in real time over Server-Sent Events.
One poll is left. Traefik’s HTTP provider only knows how to ask, so it asks the hub every few seconds, and the hub answers from memory.
The reconnect story is the part I care about most. Kill the central tether server and the tetherd agents reconnect the moment it comes back, then push their state again. There is nothing stored that could get corrupted or desync.
The setup
1. The gateway server
Run this on the machine that handles the public IP.
services:
tether:
image: ghcr.io/mizuchilabs/tether:latest
ports:
- 3000:3000
environment:
- TETHER_TOKEN=your-secret-password
Then point Traefik at it:
providers:
http:
endpoint: 'http://tether:3000/config'
pollInterval: '5s'
headers:
Authorization: 'Bearer your-secret-password'
2. The worker nodes
Run this on any server that hosts applications.
services:
tetherd:
image: ghcr.io/mizuchilabs/tetherd:latest
network_mode: host # Needed to accurately grab the host IP
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- TETHERD_SERVER=http://<TETHER_SERVER_IP>:3000
- TETHERD_TOKEN=your-secret-password
3. Deploy an app
Apps on worker nodes use standard Traefik labels, nothing else.
services:
my-app:
image: nginx
ports:
- '8080:80'
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.my-app.rule=Host(`myapp.example.com`)'
The tetherd agent replaces the internal container IP with the worker node IP before it pushes, so Traefik routes from the gateway straight to the right machine.
What it does not do
Being stateless is the tradeoff I chose. There is no history and no audit trail. After a hub restart, the routing table is rebuilt from whatever the agents push, which is the current state of the running containers and nothing more. That is enough for my setup, but it means tether cannot tell you what a route looked like yesterday.
I stopped maintaining a static IP list when I switched to this.