AI News Hub Logo

AI News Hub

Zero-Config DNS and Monitoring for Your Traefik Homelab

DEV Community
Luc Allaire

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. 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. 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 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. The mesh.routers.* namespace sits outside Traefik's schema validator. Fallback hierarchy: mesh.routers..kuma. ← highest priority mesh.routers.. mesh.kuma. mesh. ← 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. 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. 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. 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.