<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luc Allaire</title>
    <description>The latest articles on DEV Community by Luc Allaire (@wolf361).</description>
    <link>https://dev.to/wolf361</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F954241%2Fb6a8793a-3924-4e43-90b4-b31b7ca84a47.jpg</url>
      <title>DEV Community: Luc Allaire</title>
      <link>https://dev.to/wolf361</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wolf361"/>
    <language>en</language>
    <item>
      <title>Zero-Config DNS and Monitoring for Your Traefik Homelab</title>
      <dc:creator>Luc Allaire</dc:creator>
      <pubDate>Fri, 08 May 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/wolf361/zero-config-dns-and-monitoring-for-your-traefik-homelab-2ph2</link>
      <guid>https://dev.to/wolf361/zero-config-dns-and-monitoring-for-your-traefik-homelab-2ph2</guid>
      <description>&lt;p&gt;Every Traefik service you expose already has a &lt;code&gt;Host()&lt;/code&gt; rule that declares its public hostname. That information exists exactly once — in a Docker label — and propagates nowhere useful.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;traefik-mesh-companion&lt;/strong&gt; makes the container definition the single source of truth and syncs the rest automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;A Go sidecar that watches the Docker socket and syncs your Traefik routing labels to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NetBird&lt;/strong&gt; — internal mesh VPN DNS records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt; — A records or CNAMEs to a CF Tunnel endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uptime Kuma&lt;/strong&gt; — monitors, status page groups, tags, domain bindings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gatus&lt;/strong&gt; (via Gatus Bridge) — endpoints and groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single Docker Compose sidecar. No Kubernetes, no Helm, no operator.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Split-Horizon DNS via Entrypoints
&lt;/h3&gt;

&lt;p&gt;No new label namespace for DNS routing. Two env vars filter your existing entrypoint labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;INTERNAL_FILTER=internal&lt;/span&gt;   &lt;span class="c1"&gt;# routers on this entrypoint → NetBird&lt;/span&gt;
&lt;span class="s"&gt;EXTERNAL_FILTER=https&lt;/span&gt;      &lt;span class="c1"&gt;# routers on this entrypoint → Cloudflare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your existing Traefik labels stay exactly as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Matches INTERNAL_FILTER → NetBird only&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.dashboard.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Host(`dashboard.internal.example.com`)"&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.dashboard.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;

&lt;span class="c1"&gt;# Matches EXTERNAL_FILTER → Cloudflare only&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.api.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Host(`api.example.com`)"&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.api.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Force-override per container if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mesh.dns.internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;            &lt;span class="c1"&gt;# exclude from internal pipeline&lt;/span&gt;
&lt;span class="na"&gt;mesh.routers.admin.managed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;   &lt;span class="c1"&gt;# exclude this router from everything&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Rule Parser
&lt;/h3&gt;

&lt;p&gt;Pure-Go regex AST. Handles compound rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`a.example.com`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`b.example.com`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;PathPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`/v2`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both hostnames extracted for DNS. &lt;code&gt;PathPrefix&lt;/code&gt; captured separately for monitor URL construction. &lt;code&gt;HostRegexp&lt;/code&gt; intentionally skipped — you can't derive a static DNS record from a dynamic pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring Label Hierarchy
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;mesh.routers.*&lt;/code&gt; namespace sits outside Traefik's schema validator. Fallback hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mesh.routers.&amp;lt;router_name&amp;gt;.kuma.&amp;lt;property&amp;gt;   ← highest priority
mesh.routers.&amp;lt;router_name&amp;gt;.&amp;lt;property&amp;gt;
mesh.kuma.&amp;lt;property&amp;gt;
mesh.&amp;lt;property&amp;gt;                              ← lowest priority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.routers.api.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Host(`api.example.com`)"&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.api.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;

&lt;span class="na"&gt;mesh.routers.api.kuma.url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health"&lt;/span&gt;
&lt;span class="na"&gt;mesh.routers.api.kuma.accepted_status_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;204"&lt;/span&gt;
&lt;span class="na"&gt;mesh.routers.api.kuma.interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30"&lt;/span&gt;
&lt;span class="na"&gt;mesh.kuma.tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prod:green"&lt;/span&gt;
&lt;span class="na"&gt;mesh.kuma.pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public-status:APIs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tags use djb2 deterministic hashing — same tag name always maps to the same color across nodes and restarts. Override with hex: &lt;code&gt;prod:#22c55e&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mesh-companion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/wolf-infra/traefik-mesh-companion:stable&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik-mesh-companion&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SYNC_INTERVAL=1m&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LOG_LEVEL=info&lt;/span&gt;
      &lt;span class="c1"&gt;# Internal (NetBird)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;INTERNAL_PROVIDER=netbird&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;INTERNAL_FILTER=internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;INTERNAL_CLEANUP=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NETBIRD_API_TOKEN=your_netbird_token&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NETBIRD_TARGET_IP=100.64.0.5&lt;/span&gt;
      &lt;span class="c1"&gt;# External (Cloudflare)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXTERNAL_PROVIDER=cloudflare&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXTERNAL_FILTER=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXTERNAL_CLEANUP=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CLOUDFLARE_API_TOKEN=your_cf_token&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CLOUDFLARE_TARGET_DOMAIN=your-tunnel-uuid.cfargotunnel.com&lt;/span&gt;
      &lt;span class="c1"&gt;# Monitoring (Uptime Kuma)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MONITOR_PROVIDER=kuma&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_URL=http://kuma.example.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_USERNAME=admin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_PASSWORD=${KUMA_PASS}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_AUTO_ENABLE=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_GLOBAL_STATUS_PAGE=home-lab&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;stable&lt;/code&gt; — it tracks the latest release. &lt;code&gt;latest&lt;/code&gt; tracks &lt;code&gt;main&lt;/code&gt; and is explicitly experimental.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Distributed Coordinator
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Primary node&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_COORDINATOR_MODE=server&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_COORDINATOR_PORT=8081&lt;/span&gt;

&lt;span class="c1"&gt;# Other nodes&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_COORDINATOR_MODE=client&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KUMA_COORDINATOR_URL=http://primary:8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No external queue. Stateless — clients resend full state on reconnect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/wolf-infra/traefik-mesh-companion" rel="noopener noreferrer"&gt;github.com/wolf-infra/traefik-mesh-companion&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full env var reference, label override docs, and Gatus Bridge config are in the README. The &lt;code&gt;core.Processor&lt;/code&gt; interface makes adding new DNS or monitoring backends straightforward — PRs welcome.&lt;/p&gt;

&lt;p&gt;Additional DNS backends are in development — the &lt;code&gt;core.Processor&lt;/code&gt; interface is designed for exactly this. PRs welcome.&lt;/p&gt;

</description>
      <category>traefik</category>
      <category>docker</category>
      <category>go</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
