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.
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:
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.
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 networkAfter 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
.
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:
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:
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.
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.
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" ]
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!"
For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.