Skip to content
Cumps.

A Mesh of My Own: Headscale and Container Sidecars

  • Sep 15, 2026

The two mail nodes live in different data centers, Frankfurt and Zurich, which means there is no shared private network between them. Anything they say to each other crosses the public internet. The same goes for every node added since. I wanted a private network that doesn’t care where a machine physically is, and I wanted its control plane to be mine, for the same reason the whole project exists.

The answer is WireGuard, worn comfortably: Tailscale clients coordinated by Headscale, the open-source implementation of the Tailscale control server, running on my own infrastructure. Every node joins the mesh, gets a stable 100.64.x.x address, and can reach every other node over an encrypted tunnel regardless of data center, firewall, or geography. MagicDNS gives everything a name under .cumps.internal. Headplane provides a web UI over Headscale for the rare occasions I need to look at the mesh instead of just using it.

One sovereignty detail worth the effort: relay traffic. When two peers can’t connect directly, Tailscale falls back to DERP relay servers, normally operated by Tailscale Inc. I run my own DERP relay and disabled the public ones. Even in the worst-case relayed path, the packets only touch machines I control.

The sidecar pattern

Putting hosts on a mesh is standard. The design I’m actually attached to is putting every container on it individually.

Each service runs alongside a small Tailscale sidecar container, and the service container joins the sidecar’s network namespace via Docker’s network_mode: container:<sidecar>. The result: the application gets its own mesh identity, its own IP, its own MagicDNS name. Stalwart is stalwart.cumps.internal. The blog is its own hostname. So is the LDAP server, the webmail, all of it.

# The shape of it, per service:
tailscale-stalwart:   # sidecar, joins the mesh as stalwart.cumps.internal
stalwart:
  network_mode: "container:tailscale-stalwart"

The application image stays completely stock. No Tailscale baked into upstream containers, no custom builds to maintain, no network logic inside the app at all. Mesh connectivity is a separate concern bolted on from outside, which is exactly where I want it.

The payoff shows up one post from now: when the reverse proxy routes to stalwart.cumps.internal, it genuinely does not matter which physical host either container runs on. Services became location-independent names, and that single property is what later let one proxy configuration serve the whole fleet, and eventually let a NAS at home join the platform as a first-class citizen.

What it costs

Every service is now two containers, and the sidecar owns the network namespace, which creates a lifecycle coupling Docker doesn’t manage for you: recreate the sidecar and the app container is pointing at a network namespace that no longer exists. I hit this as a race condition during deploys in March, papered over it, then spent real engineering effort in May making sidecar recreation orchestrated properly across all fourteen services that use the pattern. There are enough war stories in that corner for a dedicated post later in the series.

The other cost is identity hygiene, and this one I got wrong quietly. Every node and sidecar initially joined the mesh as the user selfhost@, because it was day one and naming things felt like a problem for later. By June, “later” arrived: access control lists that can’t distinguish a mail server from a task manager aren’t access control at all. Migrating everything to tags, tag:selfhost for hosts and per-app tags like tag:app-forgejo, meant a careful two-phase ACL rollout across a live fleet. It worked, nothing broke, and it would have been thirty minutes instead of an evening if I’d started with tags on day one. Identity debt is real debt: it accrues silently and the interest compounds with every service you add.

Next week: the reverse proxy that sits in front of all of this, and why its routing table lives in a database.

About Me Author

My name is

David Cumps

A passionate geek, always curious, always learning.

You May Also Like