7 minutes
Docker mDNS Publisher
TL;DR
If you’re using Traefik with Docker and want to make your services discoverable on your local network without configuring DNS servers, you can use this script to automatically publish mDNS records:
This final version:
- Monitors Docker events for container changes
- Extracts hostnames from Traefik labels
- Uses an associative array to efficiently track and manage mDNS publishers
- Only updates records that actually change
But the journey to get there was interesting! Read on to see how I built this solution step by step.
Update: I’m working on a more generic solution: container-mdns
The Problem
I run several services at home using Docker containers with Traefik as a reverse proxy. While Traefik handles the routing based on hostnames, I still needed a way for devices on my network to discover these hostnames without:
- Maintaining a local DNS server
- Manually editing
/etc/hosts
files on each device - Using custom ports for each service
I needed a solution that would automatically maintain DNS records for my services as containers are started or stopped.
Enter mDNS with Avahi
mDNS (multicast DNS) is perfect for local network service discovery. It’s the technology behind Bonjour/Zeroconf that lets you access devices with .local
domains.
Avahi is the Linux implementation of mDNS, and it provides tools like avahi-publish
that can dynamically announce hostnames on your network.
The Solution: An Evolutionary Approach
I took an iterative approach to solving this problem, starting with a simple solution and improving it as I learned more.
Iteration 1: The Basic Script
My first attempt was simple but effective. I wrote a script that:
- Gets a list of hostnames from Traefik’s router rules in running containers
- Publishes these hostnames as mDNS records using Avahi
- Tracks the PIDs of the publisher processes in a simple array
Here’s the core of that initial approach:
#!/bin/bash
IP=$(hostname -I | awk '{print $1}')
PUBLISHERS=() # Just a simple array for PIDs
function get_hostnames() {
docker inspect $(docker ps -q) 2>/dev/null \
| jq -r '.[].Config.Labels
| to_entries[]?
| select(.key | test("^traefik\\.http\\.routers\\..*\\.rule$"))
| .value' \
| grep -oE 'Host\(`[^`]+`\)' \
| sed -E 's/Host\(`([^`]+)`\)/\1/' \
| sort -u
}
function refresh_publishers() {
# Kill all existing publishers
for pid in "${PUBLISHERS[@]}"; do
kill "$pid" 2>/dev/null
done
PUBLISHERS=()
# Start new publishers for all hostnames
local hostnames=($(get_hostnames))
for hostname in "${hostnames[@]}"; do
echo "Starting mDNS for $hostname -> $IP"
avahi-publish -a "$hostname" -R "$IP" &
PUBLISHERS+=($!)
done
}
# Initial setup
refresh_publishers
# Keep the script running
while true; do
sleep 60
refresh_publishers
done
This worked! Every minute, it would kill all the existing publishers and create new ones based on the current set of containers. But it had several issues:
- It polled every 60 seconds rather than responding to events
- It killed and recreated ALL publishers even when only one container changed
- It didn’t handle script termination gracefully
Iteration 2: Event-Driven Updates
Next, I improved the script to watch Docker events instead of polling:
function docker_event_watcher() {
docker events --filter 'event=start' --filter 'event=stop' --filter 'event=die' --filter 'event=destroy' |
while read -r event; do
echo "Docker event: $event"
# Still using the inefficient approach of refreshing everything
refresh_publishers
done
}
# Replace the polling loop with an event watcher
docker_event_watcher
This was already better - now the script would update the mDNS records immediately when containers were started or stopped. But I quickly noticed that during system shutdown or Docker restarts, multiple events would fire rapidly.
Iteration 3: Adding a Sleep Timer
To prevent the script from thrashing when multiple events occur in quick succession, I added a simple sleep timer:
function docker_event_watcher() {
docker events --filter 'event=start' --filter 'event=stop' --filter 'event=die' --filter 'event=destroy' |
while read -r event; do
echo "Docker event: $event"
sleep 1 # <-- Critical addition!
refresh_publishers
done
}
The sleep 1
is critical here. When the system is shutting down or Docker is restarting, multiple containers might stop in quick succession. Without the sleep timer, the script might try to refresh publishers too rapidly, potentially causing resource contention issues. The sleep ensures that we batch these events together and only refresh the publishers once things have settled.
Iteration 4: Efficient Publisher Management with Associative Arrays
As I began to use this script in production, I realized the approach was inefficient. For a long-lived process, killing and recreating all publishers whenever any container changed wasn’t ideal. So I refactored the code to use an associative array that would track which hostname was published by which process:
# More efficient approach using associative arrays
declare -A PUBLISHERS # hostname -> PID
function start_publisher() {
local hostname="$1"
echo "Starting mDNS for $hostname -> $IP"
avahi-publish -a "$hostname" -R "$IP" &
PUBLISHERS["$hostname"]=$!
}
function stop_publisher() {
local hostname="$1"
local pid="${PUBLISHERS[$hostname]}"
if [[ -n "$pid" ]]; then
echo "Stopping mDNS for $hostname (PID $pid)"
kill "$pid" 2>/dev/null
unset PUBLISHERS["$hostname"]
fi
}
function refresh_publishers() {
local new_hostnames=($(get_hostnames))
local current_hostnames=("${!PUBLISHERS[@]}")
local to_add=()
local to_remove=()
# Determine what to add
for h in "${new_hostnames[@]}"; do
[[ -z "${PUBLISHERS[$h]}" ]] && to_add+=("$h")
done
# Determine what to remove
for h in "${current_hostnames[@]}"; do
if ! [[ " ${new_hostnames[*]} " =~ " $h " ]]; then
to_remove+=("$h")
fi
done
for h in "${to_remove[@]}"; do stop_publisher "$h"; done
for h in "${to_add[@]}"; do start_publisher "$h"; done
}
With this approach, the script only starts or stops publishers for hostnames that have actually changed. This is much more efficient and reduces network traffic from unnecessary mDNS announcements.
Iteration 5: Proper Cleanup on Exit
Finally, I added proper cleanup handling to ensure all publisher processes were terminated when the script was stopped:
function cleanup() {
echo "Cleaning up mDNS publishers..."
for h in "${!PUBLISHERS[@]}"; do
stop_publisher "$h"
done
if [[ "$WATCHER_PID" -gt 0 ]]; then
kill "$WATCHER_PID" 2>/dev/null
fi
exit 0
}
trap cleanup SIGINT SIGTERM
The Final Solution
After several iterations, the final script puts all these components together for a robust, efficient solution:
- Uses associative arrays to track publishers by hostname
- Responds to Docker events in real-time
- Handles batches of events with a sleep timer
- Cleans up properly when terminated
Here’s how the event watcher and main script execution looks in the final version:
function docker_event_watcher() {
docker events --filter 'event=start' --filter 'event=stop' --filter 'event=die' --filter 'event=destroy' |
while read -r event; do
echo "Docker event: $event"
sleep 1 # Critical to batch rapid events together
refresh_publishers
done
}
# Main script execution
IP=$(hostname -I | awk '{print $1}')
declare -A PUBLISHERS # hostname -> PID
WATCHER_PID=0
trap cleanup SIGINT SIGTERM
# Initial setup of publishers
refresh_publishers
# Start the event watcher in the background
docker_event_watcher &
WATCHER_PID=$!
# Wait on docker watcher only
wait "$WATCHER_PID"
This approach results in a resilient script that efficiently manages mDNS records for Docker containers running behind Traefik.
Running as a Service
For reliability, you should run this as a systemd service. Create a file at /etc/systemd/system/mdns-publisher.service
:
[Unit]
Description=Dynamic mDNS publisher for Docker-hosted services
After=network.target avahi-daemon.service
Requires=avahi-daemon.service
PartOf=avahi-daemon.service
BindsTo=avahi-daemon.service
[Service]
Type=simple
ExecStart=/opt/mdns-publisher/mdns-publisher.sh
Restart=on-failure
RestartSec=3
User=root
[Install]
WantedBy=multi-user.target
Then enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now mdns-publisher.service
Benefits
With this solution:
- All your Traefik-managed services are automatically discoverable on the local network
- No need to manage DNS servers or edit hosts files
- When containers are added/removed, DNS records update automatically
- Works across different devices and operating systems that support mDNS
Prerequisites
- Docker
- Avahi (install with
apt-get install avahi-utils
on Ubuntu/Debian) - jq (for JSON parsing)
- Traefik with Docker provider
Now all my Docker services are automatically discoverable on my home network without any additional configuration on client devices. Let me know if you find this useful or have ideas for improvements!
Note: An alternative to pulling hostnames from Traefik labels is to use define a static list of hostnames (after all, normal people rarely create new containers every day). You can then use a simple array in the script to manage those hostnames instead of pulling them dynamically from Docker (or even getting them from the
compose.yml
files usingyq
). This would simplify the script further, but I prefer the dynamic approach for flexibility.
Provisioning and orchestration
1336 Words
2025-06-11 09:15 (Last updated: 2025-08-03 20:02)
5f72bf6 @ 2025-08-03