cd ..
tether(1) Mizuchi Labs Manual tether(1)

NAME

Distributed Traefik with Tether

DATE

3 min read

TAGS

#traefik #docker #homelab #golang #networking #websockets

If you host services at home or across a few VPS instances, you eventually hit a very specific networking wall: You only have one public IP address.

This means you can only port forward 443 to a single machine. If you have containers spread across different Proxmox nodes, a Raspberry Pi, or another server, you need a central reverse proxy to catch all the traffic and route it to the right internal IPs.

With Traefik, this usually means writing a massive, centralized static configuration file, or dealing with heavy service discovery tools like Consul or Redis. And since Traefik only supports a single HTTP provider endpoint, you can’t just have multiple servers send their configs individually.

I wanted something incredibly fast, completely stateless (no SQLite, no databases, no user accounts), and zero-maintenance.

So, I built tether (the central hub) and tetherd (the worker agent).

How Tether Works

The setup is split into two lightweight parts that communicate via WebSockets:

  • tether: Sits next to your central Traefik instance. It acts as the single HTTP provider endpoint that Traefik reads from. It merges incoming configurations into one unified routing table.
  • tetherd: A tiny daemon that runs on your worker servers. It watches the local Docker socket for standard Traefik labels.

Event-Driven, Not Polling

The biggest problem with older solutions (and my own previous iterations) was polling and bloat.

tether and tetherd are purely event-driven. The agents are smart: they automatically detect the host IP, parse the Traefik labels, and only push an update over the WebSocket connection if a relevant port is actually exposed or changed.

If a container starts on a worker node, the agent instantly pushes the route to the central tether server. Traefik picks it up immediately. The UI updates in real-time using Server-Sent Events (SSE).

Because the connection is persistent, it’s incredibly resilient. You can kill the central tether server, and the moment it comes back online, all the tetherd agents will immediately reconnect and push their state. No databases to get corrupted, no state to go out of sync.

The Setup

1. The Gateway Server (Tether)

Run this on the machine that handles your public IP.

services:
  tether:
    image: ghcr.io/mizuchilabs/tether:latest
    ports:
      - 3000:3000
    environment:
      - TETHER_TOKEN=your-secret-password

And configure your Traefik to read from it:

providers:
  http:
    endpoint: 'http://tether:3000/config'
    pollInterval: '5s'
    headers:
      Authorization: 'Bearer your-secret-password'

2. The Worker Nodes (Tetherd)

Run this on any server where you host 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

Now, you just deploy apps on your worker nodes exactly like you normally would. No special networks, no weird configurations. Just standard Traefik labels.

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 automatically replaces the internal container IP with the actual Worker Node’s IP, pushes it to the hub, and your traffic flows seamlessly from the Gateway to the Worker.

If you have a multi-node setup and you’re tired of maintaining static IP lists or heavy orchestrators, this completely solves the problem.

Check it out on GitHub →