mDNS and Avahi: Local Service Discovery That Works Until It Doesn't
How .local names resolve themselves, and the day they suddenly stop

Contents
You have done this a hundred times without thinking about it. You plug a Raspberry Pi into the network, type ssh [email protected], and it just works — no DNS server, no editing /etc/hosts, no hunting for an IP that DHCP handed out semi-randomly ten minutes ago. That .local resolution is multicast DNS (mDNS), and on Linux the daemon doing the heavy lifting is almost always Avahi. It is genuinely brilliant when it works, and genuinely maddening on the days it doesn’t — mostly because nobody ever explains what it’s actually doing under the surface, so when it breaks you’re left poking at it blind. This is the explanation I wish I’d had the first time a Chromecast vanished the day after I segmented my network.
What mDNS is, in one breath
Ordinary DNS is a hierarchy of servers you query by unicast: your resolver asks a chain of authoritative servers and gets an answer back. mDNS throws all of that out. There is no server. When your laptop wants to find a host on the .local domain, it sends a query to a reserved link-local multicast group on UDP port 5353, and every device on the segment hears it. The machine that owns that name answers — also by multicast, so everyone on the segment sees the reply — and now the whole subnet has cached the result. It’s gossip, not a registry. There’s no central authority to configure, nothing to run, nothing to break in the usual DNS sense.
The whole .local top-level domain is reserved by RFC 6762 for exactly this purpose, which is precisely why you must never use .local for real internal unicast DNS. If you point your internal domain at .local and also have mDNS on the wire, the two name-resolution systems will fight, and you’ll get intermittent, host-dependent, utterly baffling failures. Use mylab.example or a real subdomain for internal DNS; leave .local to mDNS alone.
Layered on top of mDNS is DNS-SD (DNS Service Discovery), the protocol behind Apple’s Bonjour. It’s how AirPlay receivers, printers, and Chromecasts announce themselves. A device doesn’t just claim a hostname; it advertises services — “I am an _ipp._tcp printer at this host, on port 631, and here’s a TXT record listing my capabilities.” Your operating system browses for those service types and populates the printer dialog, the cast menu, or the file-share list without you typing a single address. When you pick a printer from a list that “just appeared,” DNS-SD is what filled that list.
Avahi in practice
On Debian or Ubuntu, avahi-daemon is usually already running out of the box. The command-line tools that ship with it are the fastest way to see whether the magic is actually happening on your wire:
| |
Here -a browses all service types, -r resolves each advertisement to a host and address, and -t terminates after the initial sweep instead of watching live forever. If a device you expect is missing from this list, the problem is upstream of your OS — the advertisement isn’t reaching you at all, which points at the network rather than your machine. To resolve a single hostname without dragging in a full DNS stack:
| |
Publishing your own service is a one-file affair. Drop this into /etc/avahi/services/ssh.service and Avahi will advertise your SSH daemon to the whole segment:
| |
%h expands to the machine’s hostname. No restart is needed — Avahi watches that directory and picks up the new file within seconds. That’s the whole appeal: zero-configuration discovery that follows a machine around even when its address changes.
One detail people miss: Avahi advertises over both IPv4 and IPv6 by default, and modern clients frequently prefer the IPv6 (mdns6) answer. If you’ve only enabled mdns4_minimal in nsswitch but the responder hands back an IPv6 address the client then can’t route to, you get the peculiar failure where the name resolves but the connection times out. On a dual-stack network, either enable the IPv6 path properly or, if you have no working IPv6, tell Avahi to stop advertising it — mismatched stacks are a subtle, recurring source of “it resolves but won’t connect.”
mDNS inside containers, and why it often just doesn’t
If you self-host with Docker, this is the wall you’ll hit. A container on the default bridge network lives on its own little subnet behind NAT, and — say it with me — multicast does not cross that boundary. An Avahi daemon inside a bridged container simply cannot see the mDNS traffic on your LAN, so a containerised service that’s supposed to advertise itself (a media server announcing DLNA, say, or anything expecting to be discovered by Bonjour) is invisible to the rest of the house.
The blunt fix is to run that container with network_mode: host, putting it directly on the host’s network stack where the host’s own multicast reaches it. That works but throws away network isolation, so weigh it. The tidier alternative is to run a single Avahi instance on the host and let it advertise on the containers’ behalf, or to use a purpose-built mDNS reflector that bridges the container network to the LAN. Whichever you choose, the mental model is the one constant: multicast stays inside its broadcast domain, and every fix is really just a way of ferrying it across a boundary it would never cross on its own.
The day it stops working
Here is where the romance ends. mDNS relies on multicast reaching every relevant device, and a depressing number of ordinary networking decisions quietly break that assumption. When discovery fails, it’s almost always one of these four.
Subnets and VLANs. Multicast does not cross routers. The moment your IoT VLAN and your laptop VLAN are separate broadcast domains, .local discovery stops dead between them — by design, not by bug. This is the single most common cause of “my Chromecast disappeared after I segmented the network.” The fix is an mDNS reflector, and Avahi can do it itself. Edit /etc/avahi/avahi-daemon.conf:
| |
Restart the daemon and Avahi relays mDNS packets between the interfaces it’s bound to, stitching your VLANs back together for discovery purposes while leaving the rest of your segmentation intact. Many home routers and OpenWrt call this same feature an “mDNS repeater” or “Bonjour reflection”; UniFi gear exposes it as a per-network toggle. Whatever it’s named, it’s doing the same job of forwarding multicast across a boundary it would otherwise never cross. If you run managed switching and VLANs, this is a setting you’ll meet again — it’s part of the same “convenience versus segmentation” tension covered in UniFi for the home lab, where it shines and where it doesn’t.
WiFi power saving and IGMP snooping. Cheap switches with IGMP snooping enabled but no querier configured will prune multicast groups aggressively and drop mDNS on the floor. Meanwhile, aggressive WiFi power-save modes let wireless clients doze through the multicast advertisements entirely. Both produce the same maddening symptom: a device is reachable by IP but its .local name resolves intermittently, or only right after you wake it, or only from wired hosts. If wired discovery works and wireless doesn’t, suspect power-save; if nothing on a switch works, suspect the IGMP querier.
The resolver order. On systemd hosts, name resolution runs through nsswitch.conf. If mdns4_minimal isn’t in the hosts: line, then getent hosts mylab.local fails even with Avahi running perfectly on the wire:
| |
Avahi can be answering flawlessly on the network while glibc never even thinks to ask it, because nothing told the resolver that .local names go to mDNS. This is the failure that fools everyone at least once.
Duplicate names and firewalls. Two devices claiming the same hostname triggers mDNS conflict resolution, and one of them gets silently renamed with a numeric suffix appended to its .local name without telling you, so the name you typed now points nowhere. And a host firewall that blocks UDP 5353 will happily let you browse outward while never receiving the replies. Both are worth a glance when a single specific host misbehaves while everything else on the segment is fine.
Troubleshooting: bisecting the failure fast
The single most useful diagnostic move is to compare avahi-resolve against normal name resolution, because they take different paths:
avahi-resolve -n mylab.localtalks to the Avahi daemon directly, bypassingnsswitch.ssh myhostorgetent hosts mylab.localgoes through glibc and nsswitch.
So the logic is simple. If avahi-resolve succeeds but ssh myhost fails, your bug is in nsswitch.conf, not on the network — add mdns4_minimal and move on. If avahi-resolve itself fails, the advertisement isn’t reaching your machine at all, and the problem is upstream: a VLAN boundary, IGMP snooping, WiFi power-save, or a firewall dropping 5353. That one comparison eliminates half the search space in about ten seconds, and it’s the first thing I reach for every time.
For anything you genuinely depend on — a server you SSH into daily, a service other machines call by name — don’t rely on mDNS at all. Give it a real DNS entry as well, so that when discovery has an off day the dependency doesn’t. If you’re running the kind of homelab where naming and reachability matter, it’s worth reading how the same “convenience until it isn’t” trade-off plays out one layer up in mTLS, mutual TLS between services without a service mesh: both mDNS and inter-service trust are lovely when the assumptions hold and instructive to debug when they don’t.
Verdict
For a single flat home network, mDNS via Avahi is a quiet miracle — zero configuration, names that follow machines around even when DHCP reshuffles their addresses, printers and TVs that appear by themselves. Lean on it freely; there’s nothing to gain by fighting it. The instant you introduce VLANs, mesh WiFi, or managed switches, though, demote it in your head from infrastructure to fragile convenience: enable the reflector where you need cross-segment discovery, fix your IGMP querier, check nsswitch when a name won’t resolve, and keep a real DNS entry as a fallback for anything you actually rely on. It works until it doesn’t — and now you know exactly which knob to reach for when it doesn’t, and roughly why the knob exists. That’s the whole difference between mDNS feeling like magic that occasionally betrays you and mDNS feeling like a protocol with well-understood limits you can work around on purpose.




