Every Traefik service you expose already has a Host() rule that declares its public hostname. That information exists exactly once — in a Docker label — and propagates nowhere useful.
So you end up maintaining three or four systems by hand: Cloudflare for public DNS, NetBird for internal VPN-only hostnames, Uptime Kuma for monitoring — with groups, tags, and status pages configured per service. Add a container and you need to update everything manually. Remove it 4 months later and those records stay unless you remember to clean them up.
traefik-mesh-companion makes the container definition the single source of truth and syncs the rest automatically.
What It Does
A Go sidecar that watches the Docker socket and syncs your Traefik routing labels to:
- NetBird — internal mesh VPN DNS records
- Cloudflare — A records or CNAMEs to a CF Tunnel endpoint
- Uptime Kuma — monitors, status page groups, tags, domain bindings
- Gatus (via Gatus Bridge) — endpoints and groups
A single Docker Compose sidecar. No Kubernetes, no Helm, no operator.
How It Works
Split-Horizon DNS via Entrypoints
No new label namespace for DNS routing. Two env vars filter your existing entrypoint labels:
INTERNAL_FILTER=internal # routers on this entrypoint → NetBird
EXTERNAL_FILTER=https # routers on this entrypoint → Cloudflare
Your existing Traefik labels stay exactly as-is:
# Matches INTERNAL_FILTER → NetBird only
traefik.http.routers.dashboard.rule: "Host(`dashboard.internal.example.com`)"
traefik.http.routers.dashboard.entrypoints: internal
# Matches EXTERNAL_FILTER → Cloudflare only
traefik.http.routers.api.rule: "Host(`api.example.com`)"
traefik.http.routers.api.entrypoints: https
Force-override per container if needed:
mesh.dns.internal: "false" # exclude from internal pipeline
mesh.routers.admin.managed: "false" # exclude this router from everything
The Rule Parser
Pure-Go regex AST. Handles compound rules:
(Host(`a.example.com`) || Host(`b.example.com`)) && PathPrefix(`/v2`)
Both hostnames extracted for DNS. PathPrefix captured separately for monitor URL construction. HostRegexp intentionally skipped — you can't derive a static DNS record from a dynamic pattern.
Monitoring Label Hierarchy
The mesh.routers.* namespace sits outside Traefik's schema validator. Fallback hierarchy:
mesh.routers.<router_name>.kuma.<property> ← highest priority
mesh.routers.<router_name>.<property>
mesh.kuma.<property>
mesh.<property> ← lowest priority
Real example:
traefik.http.routers.api.rule: "Host(`api.example.com`)"
traefik.http.routers.api.entrypoints: https
mesh.routers.api.kuma.url: "/health"
mesh.routers.api.kuma.accepted_status_codes: "200, 204"
mesh.routers.api.kuma.interval: "30"
mesh.kuma.tags: "backend, prod:green"
mesh.kuma.pages: "public-status:APIs"
Tags use djb2 deterministic hashing — same tag name always maps to the same color across nodes and restarts. Override with hex: prod:#22c55e.
Quick Start
services:
mesh-companion:
image: ghcr.io/wolf-infra/traefik-mesh-companion:stable
container_name: traefik-mesh-companion
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- SYNC_INTERVAL=1m
- LOG_LEVEL=info
# Internal (NetBird)
- INTERNAL_PROVIDER=netbird
- INTERNAL_FILTER=internal
- INTERNAL_CLEANUP=true
- NETBIRD_API_TOKEN=your_netbird_token
- NETBIRD_TARGET_IP=100.64.0.5
# External (Cloudflare)
- EXTERNAL_PROVIDER=cloudflare
- EXTERNAL_FILTER=https
- EXTERNAL_CLEANUP=true
- CLOUDFLARE_API_TOKEN=your_cf_token
- CLOUDFLARE_TARGET_DOMAIN=your-tunnel-uuid.cfargotunnel.com
# Monitoring (Uptime Kuma)
- MONITOR_PROVIDER=kuma
- KUMA_URL=http://kuma.example.com
- KUMA_USERNAME=admin
- KUMA_PASSWORD=${KUMA_PASS}
- KUMA_AUTO_ENABLE=true
- KUMA_GLOBAL_STATUS_PAGE=home-lab
Use stable — it tracks the latest release. latest tracks main and is explicitly experimental.
Advanced: Distributed Coordinator
Running multiple edge nodes writing to one Uptime Kuma? They face race conditions on status page writes — both read current state, both modify it and last write ends up stomping the other's changes.
The companion ships a built-in Distributed Coordinator. One node is the server. Clients provision monitors locally and forward status page attachment operations to the server for sequential processing.
# Primary node
- KUMA_COORDINATOR_MODE=server
- KUMA_COORDINATOR_PORT=8081
# Other nodes
- KUMA_COORDINATOR_MODE=client
- KUMA_COORDINATOR_URL=http://primary:8081
No external queue. Stateless — clients resend full state on reconnect.
Try It
GitHub: github.com/wolf-infra/traefik-mesh-companion
Full env var reference, label override docs, and Gatus Bridge config are in the README. The core.Processor interface makes adding new DNS or monitoring backends straightforward — PRs welcome.
Additional DNS backends are in development — the core.Processor interface is designed for exactly this. PRs welcome.
United States
NORTH AMERICA
Related News
Amazon Employees Are 'Tokenmaxxing' Due To Pressure To Use AI Tools
20h ago
UCP Variant Data: The #1 Reason Agent Checkouts Fail
6h ago

Décryptage technique : Comment builder un téléchargeur de vidéos Reddit performant (DASH, HLS & WebAssembly)
16h ago
How Braze’s CTO is rethinking engineering for the agentic area
10h ago
Encryption Protocols for Secure AI Systems: A Practical Guide
20h ago