What You'll Build
A zero-configuration LAN where Raspberry Pi nodes automatically discover each other by service name — no IP addresses to remember, no DNS server to maintain, no cloud dependency. Each Pi announces what it does; other Pis find it by asking "who can sync files?" or "who monitors systems?"
The Problem
Traditional networked services rely on one of three approaches, all problematic for self-hosted Pi clusters:
- Static IPs — Assign each Pi a fixed address. Breaks when DHCP leases expire or you add new nodes.
- Central DNS — Run a local DNS server. Adds complexity and a single point of failure.
- Cloud registry — Use a SaaS discovery service. Defeats the purpose of self-hosting.
mDNS (multicast DNS) offers a fourth way: decentralized, zero-config, completely local.
How mDNS Works
mDNS lets devices resolve hostnames to IP addresses without a central DNS server. When a Pi joins the network, it announces itself. When another Pi wants to connect, it asks the network "who is pi-workshop.local?" and gets the current IP.
Pi joins LAN
→
Announces hostname.local
→
Other Pis cache response
→
SSH to pi.local works
Phase 1: Install Avahi (mDNS)
Avahi is the Linux implementation of mDNS/DNS-SD. Install it on every Pi in your network.
Install Avahi on Raspberry Pi
sudo apt update && sudo apt install -y avahi-daemon avahi-utils
Reading package lists... Done
Building dependency tree... Done
✓ Avahi installed
sudo systemctl enable --now avahi-daemon
Created symlink /etc/systemd/system/dbus... → avahi-daemon
Configure Hostname
Set a memorable hostname on each Pi. This becomes hostname.local.
sudo hostnamectl set-hostname pi-workshop
sudo hostnamectl set-hostname pi-garden
sudo hostnamectl set-hostname pi-nexus
sudo reboot
Test mDNS Resolution
After reboot, verify mDNS works between nodes:
ping pi-garden.local
PING pi-garden.local (192.168.1.102) 56(84) bytes of data.
64 bytes from pi-garden.local (192.168.1.102): icmp_seq=1 ttl=64 time=0.5 ms
✓ mDNS resolution working
avahi-browse -a
+ eth0 IPv4 pi-workshop [00:1a:2b:3c:4d:5e] Workstation
+ eth0 IPv4 pi-garden [aa:bb:cc:dd:ee:ff] Workstation
Phase 2: Service Discovery (DNS-SD)
Beyond hostnames, we want to discover services — "who can sync files?" "who monitors systems?" DNS-SD (DNS Service Discovery) publishes what each Pi can do.
How rpi-sync Uses Discovery
When rpi-sync runs, it advertises itself via Avahi. Other nodes can discover it without knowing its IP:
discover_services.py
import subprocess
import re
def discover_rpi_sync_nodes():
"""Find all rpi-sync services on the local network."""
nodes = []
result = subprocess.run(
["avahi-browse", "-t", "-r",
"_rpi-sync._tcp"],
capture_output=True,
text=True
)
for line in result.stdout.splitlines():
if "=" in line and "rpi-sync" in line:
parts = line.split()
if len(parts) >= 6:
hostname = parts[3]
nodes.append({
"hostname": hostname,
"address": f"{hostname}.local"
})
return nodes
if __name__ == "__main__":
nodes = discover_rpi_sync_nodes()
for node in nodes:
print(f"Found: {node['address']}")
Publishing a Service
To make your service discoverable, create an Avahi service file:
/etc/avahi/services/rpi-sync.service
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name>Raspberry Pi File Sync</name>
<service>
<type>_rpi-sync._tcp</type>
<port>22</port>
<txt-record>version=2.0</txt-record>
<txt-record>capabilities=sync,deploy,watch</txt-record>
</service>
</service-group>
sudo systemctl reload avahi-daemon
avahi-browse -t -r _rpi-sync._tcp
= eth0 IPv4 Raspberry Pi File Sync _rpi-sync._tcp local
hostname = [pi-workshop.local]
address = [192.168.1.101]
port = [22]
txt = ["version=2.0" "capabilities=sync,deploy,watch"]
Quick feedback
Did this guide help?
Your answers shape what we write next.
Phase 3: Build a Service Mesh
With mDNS working, your Pis can form a service mesh — each node knows what every other node can do.
CoreConduit Service Mesh
pi-workshop
_rpi-sync._tcp
_rpi-monitor._tcp
⇄
pi-garden
_hydromazing._tcp
_rpi-sync._tcp
⇄
pi-nexus
_nexus-ai._tcp
_rpi-sync._tcp
Service Registry Pattern
Build a simple service registry that auto-updates as nodes join and leave:
service_registry.py
import subprocess
import json
import threading
import time
from pathlib import Path
class LANServiceRegistry:
"""Discovers and tracks services on the local network."""
def __init__(self, cache_file="~/.lan_registry.json"):
self.cache_file = Path(cache_file).expanduser()
self.services = {}
self._running = False
def discover(self, service_type=None):
"""Discover services. If type is None, discover all."""
cmd = ["avahi-browse", "-t", "-r", "-p"]
if service_type:
cmd.append(service_type)
else:
cmd.append("-a")
result = subprocess.run(cmd, capture_output=True, text=True)
return self._parse_services(result.stdout)
def get_service(self, name):
"""Get a service by name."""
self.refresh()
return self.services.get(name)
def get_by_type(self, service_type):
"""Get all services of a given type."""
return [
s for s in self.services.values()
if s.get("type") == service_type
]
def refresh(self):
"""Refresh service list from network."""
self.services = self.discover()
self._save_cache()
def _save_cache(self):
with open(self.cache_file, "w") as f:
json.dump(self.services, f, indent=2)
def _parse_services(self, output):
"""Parse avahi-browse output."""
services = {}
for line in output.strip().split("\n"):
if line.startswith("="):
parts = line.split(";")
if len(parts) >= 10:
name = parts[3]
services[name] = {
"type": parts[4],
"domain": parts[5],
"hostname": parts[6],
"address": parts[7],
"port": int(parts[8]),
"txt": parts[9:]
}
return services
Phase 4: Practical Integration
Now integrate discovery into your projects. Here's how rpi-sync uses it:
Auto-Discover Sync Peers
rpi-sync discover
Scanning LAN for rpi-sync nodes...
Found 3 nodes:
pi-workshop.local (192.168.1.101) [rpi-sync v2.1]
pi-garden.local (192.168.1.102) [rpi-sync v2.0]
pi-nexus.local (192.168.1.103) [rpi-sync v2.1]
Add discovered nodes to config? [Y/n] y
✓ Added 3 nodes to ~/.rpi-sync/rpi-sync.conf
Auto-Configure rpi-monitor Hub
When running rpi-monitor in hub mode, it can discover agents automatically:
services.json (auto-generated)
{
"agents": [
{
"name": "pi-workshop",
"host": "pi-workshop.local",
"port": 8585,
"discovered": true
},
{
"name": "pi-garden",
"host": "pi-garden.local",
"port": 8585,
"discovered": true
}
]
}
Phase 5: Troubleshooting Discovery
Discovery Not Working?
mDNS is multicast-based and can be blocked by network equipment or firewalls.
sudo systemctl status avahi-daemon
sudo ufw status | grep 5353
sudo ufw allow 5353/udp
avahi-resolve-host-name pi-workshop.local
pi-workshop.local 192.168.1.101
sudo tcpdump -i eth0 udp port 5353
Common Issues
| .local not resolving |
Install libnss-mdns: sudo apt install libnss-mdns |
| Windows can't see .local |
Install Bonjour or use the IP directly |
| Multicast blocked |
Some routers block mDNS. Enable IGMP snooping or use static IPs as fallback |
| Docker containers |
Use --network host mode or install avahi-daemon in container |
Security Considerations
mDNS operates on the local network only — it doesn't traverse routers or the internet. This is a feature for self-hosted setups, but be aware:
- LAN-only — Anyone on your network can discover your services
- No authentication — mDNS is purely informational; services must handle auth
- Spoofing possible — A malicious device could impersonate another service
For sensitive services, layer additional authentication (SSH keys, API tokens) on top of discovery.
Next Steps
Get the Code
Full implementations in rpi-sync (LAN discovery for file sync) and rpi-monitor (hub mode with auto-discovered agents).