About

System administration is a painstaking job and on Linux a lot of the administration process involves editing files because on Linux everything is a file and most of the configuring of the software is done by editing configuration files. This is a very bothersome process and requires that everything that is typed to actually be correct or a misconfiguration can lead to a whole lot of mishaps.

Imagine having to set up a few Docker containers running various software packages and in that case the job for each container would be to create a reverse-proxy entry that will map external connections onto the port that the container is listening on publicly. Depending on the needs, hostname registration and DNS setup might be required in order to be able to access the containers individually by name because remembering IP addresses is a really bad practice due to IP addresses not being stable compared to hostnames.

It would of course be very handy to be able to dynamically configure a container automatically such that no local variables are involved. This page describes a method from Wizardry and Steamworks how to setup a bunch of containers and also make them publicly available on the local network by host-name.

Architecture

In short, the wizardrysteamworks/docker-avahi-publish is started on the same machine as traefik and, in turn traefik is configured to automatically route any containers using the syntax:

CONTAINER_NAME.TLD

where:

  • CONTAINER_NAME is the name of the container as started from Docker with the –name parameter,
  • TLD is an mDNS TLD, by default local used for the mDNS name resolution

This makes it such that after the container, traefik and wizardrysteamworks/docker-avahi-publish, any user on the network can then just access the container using its name and the configured mDNS domain. For example, starting Sonarr, traefik and 'wizardrysteamworks/docker-avahi-publish on the same machine will automatically make Sonarr available at http://sonarr.local without any configuration required at all. In fact, once traefik and wizardrysteamworks/docker-avahi-publish is running, any other container, for example Lidarr, could be started by passing as parameter –name lidarr and then the container will be available at http://lidarr.local.

Note that all of that occurs without bothering about:

  • port numbers,
  • name to IP mappings,
  • messing with reverse-proxy configurations

and that essentially any and all containers started on the same machine as wizardrysteamworks/docker-avahi-publish will be available on the local mDNS address.

Whilst ISC BIND is the referential domain name technology out there, other solutions exist such as mDNS that compared to ISC BIND are designed to be more flexible in terms of dynamism as well as having the capability to referring to "services" rather than "machines" which is what ISC BIND accomplishes.

The job of wizardrysteamworks/docker-avahi-publish is to listen to Docker events and then check for containers that are started, containers that are stopped and then advertise the container name when the container is started, respectively stop advertising the container name when the container is stopped. wizardrysteamworks/docker-avahi-publish is similar to docker-hoster that accomplishes the task of synchronizing the /etc/host file on a Docker host with the container names that are running on it.

Running on the Command Line

wizardrysteamworks/docker-avahi-publish can be started directly from the command line without even setting it up as a docker compose file. The requirement is that a macvlan network is first established. In order to do so, create a mcvlan network by issuing:

docker network create -d macvlan --subnet 192.168.0.0/16 --gateway 192.168.1.1 -o parent=eth0 docker_macvlan

where:

  • 192.168.0.0/16 is a network with the subnet range from 192.168.0.1 to 192.168.255.254,
  • 192.168.1.1 is the LAN gateway,
  • eth0 is the default network interface,
  • docker_macvlan is a descriptive name for the network

After that, the command:

docker run \
    -it \
    --rm \
    --net=docker_macvlan \
    --interactive \
    --ip=192.168.0.1 \
    -e POINTER_ADDRESS=192.168.1.5 \
    -v /var/run/docker.sock:/var/run/docker.sock \
    wizardrysteamworks/docker-avahi-publish

where:

  • POINTER_ADDRESS points to the machine that is running traefik,
  • 192.168.0.1 is an IP address for the container itself that is publishing mDNS names and should be chosen as an IP address that does not fall within any DHCP network-range on the local network (in this case, the macvlan network was created as 192.168.0.0/16 with the actual Docker LAN network being 192.168.1.0/24 such that it is never possible that the DHCP server will allocate the address 192.168.0.1)

Here is some example output from the terminal:

waiting for dbus socket: .
dbus is running!
starting avahi daemon...
waiting for container events: Found user 'avahi' (UID 101) and group 'avahi' (GID 102).
Successfully dropped root privileges.
avahi-daemon 0.8 starting up.
Successfully called chroot().
Successfully dropped remaining capabilities.
No service file found in /etc/avahi/services.
Joining mDNS multicast group on interface eth0.IPv4 with address 192.168.0.1.
New relevant interface eth0.IPv4 for mDNS.
Joining mDNS multicast group on interface lo.IPv6 with address ::1.
New relevant interface lo.IPv6 for mDNS.
Joining mDNS multicast group on interface lo.IPv4 with address 127.0.0.1.
New relevant interface lo.IPv4 for mDNS.
Network interface enumeration completed.
Registering new address record for 192.168.0.1 on eth0.IPv4.
Registering new address record for ::1 on lo.*.
Registering new address record for 127.0.0.1 on lo.IPv4.
Server startup complete. Host name is 12225d4c2b98.local. Local service cookie is 293021545.
........
stopping advertisements for sonarr running under PID
..
advertising sonarr.local as 192.168.1.5
Server version: avahi 0.8; Host name: 12225d4c2b98.local
Established under name 'sonarr.local'
........................................................................................................

What can be observed from the output is that wizardrysteamworks/docker-avahi-publish first tries to remove the sonarr.local (due to the restart) mapping and when the sonarr container is brought back up, wizardrysteamworks/docker-avahi-publish will map it again to the IP address specified in POINTER_ADDRESS.

Practical Application

Imagine that now a whole Servarr stack should be setup such that all the Servarr components like Sonarr, Radarr, Readarr, etc, should be available via their mDNS hostnames like http://sonarr.local, http://radarr.local, and so on, such that the whole stack is available directly as the containers are (re)started.

Setting everything up can be broken down into the following tasks:

  • setting up the container whose names will have to be published (repeatable for all containers),
  • setting up traefik,
  • setting up wizardrysteamworks/docker-avahi-publish

For the first step, the only requirement is to ensure that a container has a name when it is started. This is done by passing the –name parameter to the container once it is started. Here is an example service file that starts the gotify notification service:

[Unit]
Description=Gotify
After=docker.service traefik.service docker-avahi-publish
Requires=docker.service
StartLimitIntervalSec=0
Wants=traefik.service docker-avahi-publish.service
 
[Service]
Restart=always
RestartSec=5s
ExecStartPre=/bin/sh -c '/usr/bin/docker network create entertainment || true'
ExecStartPre=/usr/bin/docker pull gotify/server
ExecStart=/usr/bin/docker run --name=gotify \
  --rm \
  --hostname gotify \
  --net=entertainment \
  --interactive \
  --user 0:0 \
  --health-cmd 'curl -f http://localhost:80/health || exit 1' \
  --health-interval 1m \
  --health-retries 3 \
  --health-timeout 10s \
  --health-start-period 60s \
  -e TZ=Etc/UTC \
  -p 50111:80 \
  -v /mnt/swarm/docker/data/gotify:/app/data \
  gotify/server
ExecStop=/usr/bin/docker stop gotify
ExecStop=/usr/bin/docker rm -f gotify
TimeoutSec=300
Environment=DOCKER_CONFIG=/etc/docker
 
[Install]
WantedBy=multi-user.target

where, for this application, all that is required is the –name=gotify part of the service file.

For the second part, traefik has to be setup and its default route for Docker has to be set to include the local domain name. Here is a SystemD service file that starts traefik:

[Unit]
Description=Traefik
After=docker.service docker-avahi-publish.service
Requires=docker.service
StartLimitIntervalSec=0
Wants=docker-avahi-publish.service
 
[Service]
Restart=always
RestartSec=5s
ExecStartPre=/bin/sh -c '/usr/bin/docker network create entertainment || true'
ExecStartPre=/usr/bin/docker pull traefik:v3.4
ExecStart=/usr/bin/docker run --name=traefik \
  --rm \
  --hostname traefik \
  --net=entertainment \
  --interactive \
  --user 0:0 \
  -p 80:80 \
  -p 8080:8080 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  traefik:v3.4 \
  --api.insecure=true \
  --providers.docker=true \
  --entrypoints.web.address=:80 \
  --log=true --log.level=INFO \
  --providers.docker.exposedbydefault=true \
  --providers.docker.defaultrule="Host(`{{ normalize .Name }}.local`)"
ExecStop=/usr/bin/docker stop traefik
ExecStop=/usr/bin/docker rm -f traefik
TimeoutSec=300
Environment=DOCKER_CONFIG=/etc/docker
 
[Install]
WantedBy=multi-user.target

where:

  • the default rule for reverse-proxying consists in Host(`normalize_.name.local`) which corresponds to the container name followed by the suffix .local.

Note that all the former are more-or-less irrespective of wizardrysteamworks/docker-avahi-publish and should be performed anyway regardless of the mDNS broadcaster.

Finally, setting up wizardrysteamworks/docker-avahi-publish is perhaps the easiest job:

[Unit]
Description=Avahi Docker Publish
After=docker.service
Requires=docker.service
StartLimitIntervalSec=0

[Service]
Restart=always
RestartSec=5s
ExecStartPre=/bin/sh -c '/usr/bin/docker network create -d macvlan --subnet ${SUBNET} --gateway ${GATEWAY} -o parent=${INTERFACE} ${NETWORK_NAME} || true'
ExecStartPre=/usr/bin/docker pull wizardrysteamworks/avahi-docker-publish:latest
ExecStartPre=/bin/sh -c '/usr/bin/docker network create -d macvlan --subnet ${SUBNET} --gateway ${GATEWAY} -o parent=${INTERFACE} ${NETWORK_NAME} || true'
ExecStartPre=/usr/bin/docker pull wizardrysteamworks/avahi-docker-publish:latest
ExecStart=/bin/bash -c "/usr/bin/docker run --name=avahi-docker-publish \
    --rm \
    --net=${NETWORK_NAME} \
    --interactive \
    --ip=${ADDRESS} \
    -e POINTER_ADDRESS=\"$(hostname -I | cut -d' ' -f1)\" \
    -v /run/docker.sock:/run/docker.sock \
    wizardrysteamworks/avahi-docker-publish:latest"
ExecStop=/usr/bin/docker stop avahi-docker-publish
ExecStop=/usr/bin/docker rm -f avahi-docker-publish
TimeoutSec=300
Environment=DOCKER_CONFIG=/etc/docker
Environment=SUBNET=192.168.0.0/16
Environment=GATEWAY=192.168.1.1
Environment=INTERFACE=eth0
Environment=NETWORK_NAME=docker_macvlan
Environment=ADDRESS=192.168.0.1

[Install]
WantedBy=multi-user.target

where the network parameters following Environment must be changed accordingly to match the macvlan network.

Reasoning

The canonical way described within the traefik documentation is that containers would have to be labeled and then they would be accessible, for example, via the hostname CONTAINER.localhost where CONTAINER is the name of a container, yet this is obviously not feasible if the containers are to be accessed via the LAN network.

One step up from that would be to overwrite the traefik default route for docker to insert the traefik machine FQDN into the host rule, as in Host(`{{ normalize .Name }}.MYMACHINE.TLD`) with MYMACHINE.TLD being the local host and domain name. However, in that case DNS entries for MYMACHINE.TLD have to be setup, albeit even if they are wildcard DNS entries, such as when split-DNS was set up for a DuckDNS domain name. Again, this is still very feasible but the point of this is to directly start the containers and they'd be magically available to the local network facilitating a quick deployment payload that can be used on any LAN network such that setting up BIND is way out of the scope.

Another attempt would be to just sub-domain the mDNS domain local by adjusting the traefik default route to Host(`{{ normalize .Name }}.MYMACHINE.local`) where MYMACHINE is the hostname of the machine running traefik and the containers. Pedantically, this should not work because a subdomain must be queried just like the parent domain such that the inference that a subdomain's A-records must match all the A-records for the parent domain is just false in theory. Admittedly, the reasoning here would be that once the mDNS resolver would figure out that CONTAINER.MYMACHINE.local does not resolve properly, it would attempt to query MYMACHINE.local in order to find out about CONTAINER.MYMACHINE.local yet even that would be only if MYMACHINE.local would have an NS-record pointing to a resolver for the MYMACHINE.local domain which is not the case because this is mDNS.

For those reasons, registering and unregistering containers via mDNS using avahi seems to be the most elegant solution with the minimal amount of changes required.

Source Code

The container itself is pretty bland and just installs the necessary tools because all the magic takes place in Bash.

FROM debian:stable-slim
MAINTAINER Wizardry and Steamworks (wizardry.steamworks@outlook.com)
 
# update package manager
RUN  apt-get update -y && \
     apt-get upgrade -y && \
     apt-get dist-upgrade -y && \
     apt-get -y autoremove && \
     apt-get clean
 
# install required tools
RUN apt-get install -y \
    curl \
    jq \
    avahi-daemon \
    avahi-utils \
    avahi-discover
 
# UTF-8 support
RUN apt-get install -y coreutils locales && \
    sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
    locale-gen && \
    dpkg-reconfigure --frontend=noninteractive locales && \
    update-locale LANG=en_US.UTF-8
 
COPY rootfs /
 
RUN chmod +x /usr/local/bin/run
ENTRYPOINT [ "/usr/local/bin/run" ]

Script

The container sets /usr/local/bin/run as the entry point which makes all of this possible.

#!/usr/bin/env bash
###########################################################################
##  Copyright (C) Wizardry and Steamworks 2025 - License: MIT            ##
###########################################################################
#set -x
 
# check for Docker socket
[ ! -S /run/docker.sock ] && \
    echo "Docker socket file not found!" && \
    exit 1
[ -z "${POINTER_ADDRESS}" ] && \
    echo "The POINTER_ADDRESS environment variable has to be set to point to the traefik IP address." && \
    exit 1
 
# this must be set by the container
[ -z "${MDNS_DOMAIN}" ] && MDNS_DOMAIN='local'
 
# start dbus and wait for socket
dbus-uuidgen > /var/lib/dbus/machine-id
mkdir -p /run/dbus
PROCESS_OUTPUT=$(mktemp "${TMPDIR:-/tmp/}$(basename $0).XXX")
dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address --nofork &> ${PROCESS_OUTPUT} &
PROCESS_PID=$!
echo -n "waiting for dbus socket: "
until grep -q -i 'unix:path' ${PROCESS_OUTPUT}
    do       
        if ! ps ${PROCESS_PID} > /dev/null 
        then
            echo "The process died" >&2
            exit 1
        fi
        echo -n "."
        sleep 1
    done
echo 
echo "dbus is running!" 
sleep 1
 
# start Avahi mDNS Daemon in the background
echo "starting avahi daemon..."
avahi-daemon &
 
# create associative array to store publisher PIDs
declare -A PUBLISHERS
# publishers lock file
LOCK_FILE='/tmp/publishers'
function lock() {
    # acquire lock
    if mkdir "${LOCK_FILE}" &>/dev/null; then
        trap '{ rm -rf ${LOCK_FILE}; }' HUP INT QUIT TERM EXIT
    else
        exit 0
    fi
}
 
function unlock() {
    rm -rf ${LOCK_FILE}
}
 
# trap HUP and flush
function flush_mdns() {
    echo "got HUP signal..."
    for KEY in "${!PUBLISHERS[@]}"
    do
        echo "terminating advertisement for ${KEY}"
        kill -s TERM "${PUBLISHERS[${KEY}]}"
    done
}
trap 'flush_mdns' HUP
 
# starting initial pass and publishing containers
echo "starting initial startup pass..."
curl \
    -s \
    --unix-socket '/run/docker.sock' \
    -H 'Accept: application/json' \
    'http://localhost/containers/json' | jq -r '.[].Names[0]' | sed 's/\///g' | while read -r MDNS_HOST; do
 
    echo "advertising ${MDNS_HOST}.${MDNS_DOMAIN} as ${POINTER_ADDRESS}"
    avahi-publish -v -a "${MDNS_HOST}.${MDNS_DOMAIN}" -R ${POINTER_ADDRESS} &
    lock
    PUBLISHERS[${MDNS_HOST}]=$!
    unlock
done
 
# read Docker socket and start or stop publishers with containers
echo -n "waiting for container events: "
curl \
    -s \
    --unix-socket '/run/docker.sock' \
    -H "Accept: application/json" \
    'http://localhost/events' | while read -r EVENT; do
    # build name.local
    echo -n "."
    MDNS_HOST=`echo ${EVENT} | jq -r '.Actor.Attributes.name'`
    if [ -z ${MDNS_HOST} ]; then
        continue
    fi
    case `echo ${EVENT} | jq -r ".status"` in
      "create")
        printf "\n"
        echo "advertising ${MDNS_HOST}.${MDNS_DOMAIN} as ${POINTER_ADDRESS}"
        avahi-publish -v -a "${MDNS_HOST}.${MDNS_DOMAIN}" -R ${POINTER_ADDRESS} &
        lock
        PUBLISHERS[${MDNS_HOST}]=$!
        unlock
        ;;
      "destroy")
        lock
        MDNS_PID=${PUBLISHERS[${MDNS_HOST}]}
        unlock
        printf "\n"
        echo "stopping advertisements for ${MDNS_HOST} running under PID ${MDNS_PID}"
        if [ ! -z "${MDNS_PID}" ]; then
            kill -s TERM "${MDNS_PID}"
        fi
        ;;
    esac
done
 
echo "bye!"

docker/container_auto_discovery_with_mdns_and_traefik.txt · Last modified: 2025/07/17 04:17 by office

Wizardry and Steamworks

© 2025 Wizardry and Steamworks

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.