The .lan suffix is a perfectly reasonable convention for home lab services, but getting it to work cleanly — proper HTTPS, no certificate warnings, short names that resolve everywhere on the machine — takes a few moving parts. This is the setup I've landed on (Ubuntu 24.04, as of June 2026), plus the DNS priority conflict I ran into when trying to add services that the router already knew about.
The pieces
Three components work together:
- dnsmasq at
127.0.0.2— resolves every*.lanname to127.0.0.1via a single wildcard rule - systemd-resolved — routes
.lanqueries to dnsmasq, everything else upstream - Caddy v2 — listens on
127.0.0.1:443, terminates TLS, proxies to backends
The result: any .lan name resolves locally, Caddy picks it up and proxies to the right container or port, and the browser sees a valid HTTPS certificate (from Caddy's built-in CA).
dnsmasq
Install and configure:
sudo apt install dnsmasq
# /etc/dnsmasq.d/lan.conf
address=/.lan/127.0.0.1
listen-address=127.0.0.2
The listen-address=127.0.0.2 keeps dnsmasq off port 53 on the main loopback (which systemd-resolved's stub resolver already owns at 127.0.0.53). The wildcard address=/.lan/127.0.0.1 answers every *.lan query with 127.0.0.1 — no per-service DNS entries needed.
sudo systemctl enable --now dnsmasq
systemd-resolved routing domain
# /etc/systemd/resolved.conf.d/lan.conf
[Resolve]
DNS=127.0.0.2
Domains=~lan
The ~ prefix makes lan a routing domain: systemd-resolved sends .lan queries to 127.0.0.2 (dnsmasq) and nothing else. Queries for everything else go to whichever upstream DNS the network provides.
sudo systemctl restart systemd-resolved
resolvectl status | grep -A3 "Global"
You should see 127.0.0.2 listed and ~lan as the domain.
Caddy
Install from the official Cloudsmith apt repo rather than the Ubuntu package — the Ubuntu version lags significantly:
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
Trust Caddy's internal CA so the browser accepts .lan certs without a warning:
sudo caddy trust
(You'll need to import the CA into Firefox manually if you use it — the caddy trust command handles Chrome/system trust stores.)
Organising the config
Rather than one large Caddyfile, a snippet import keeps things tidy:
# /etc/caddy/Caddyfile
import /etc/caddy/sites-enabled/*.caddy
Each service gets its own file:
# /etc/caddy/sites-enabled/open-webui.caddy
open-webui.lan {
tls internal
reverse_proxy 10.140.20.61:8080
}
For a service with a self-signed upstream certificate (Proxmox web UI being the obvious example):
# /etc/caddy/sites-enabled/xenon.caddy
xenon.lan {
tls internal
reverse_proxy https://10.140.3.82:8006 {
transport http {
tls_insecure_skip_verify
}
}
}
Add a service, reload:
sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy
The DNS priority conflict
This works perfectly for services the router doesn't know about — typically anything on a local container subnet (10.140.20.x in my setup, managed by the Incus bridge's DHCP). For services elsewhere on the network whose hostnames appear in the router's DHCP lease table, there's a conflict.
systemd-resolved receives .lan queries and has two servers claiming authority: the global ~lan → 127.0.0.2 entry from resolved.conf, and the per-link entry pushed by DHCP (domain=lan, DNS=<router IP>). Per-link entries win over global entries in systemd-resolved's priority ordering. So for a hostname the router knows — say gitea.lan for a container it gave a lease to — the router answers first with the real IP, Caddy is bypassed, and you get a connection refused on port 443 (or wherever Caddy is listening).
The fix in NetworkManager is to set a positive dns-priority on the affected connections, which makes them lower priority than the global server:
sudo nmcli con mod "Wired connection 3" ipv4.dns-priority 100
sudo nmcli con mod "Wired connection 3" ipv6.dns-priority 100
sudo nmcli con up "Wired connection 3"
In my session this didn't propagate immediately (the priority change may need a full reconnect or systemctl restart systemd-resolved). The pragmatic workaround is an /etc/hosts entry for any hostname the router already knows:
echo "127.0.0.1 gitea.lan" | sudo tee -a /etc/hosts
echo "127.0.0.1 xenon.lan" | sudo tee -a /etc/hosts
Not elegant, but unambiguous — /etc/hosts is checked before DNS by glibc, so it wins regardless of what the router says.
Current service map
For reference, the full set of .lan entries currently active on this machine:
| Hostname | Backend | Notes |
|---|---|---|
open-webui.lan |
10.140.20.61:8080 |
Open WebUI (Incus container) |
litellm.lan |
10.140.20.63:4000 |
LiteLLM proxy |
searxng.lan |
10.140.20.15:8080 |
SearXNG search |
ollama.lan |
localhost:11434 |
Ollama LLM inference |
comfyui.lan |
localhost:8188 |
ComfyUI image generation |
gitea.lan |
10.140.3.116:3000 |
Gitea (via /etc/hosts) |
xenon.lan |
10.140.3.82:8006 |
Proxmox UI, TLS skip (via /etc/hosts) |
vaultwarden.lan |
10.140.20.16:8080 |
Vaultwarden |
wake.lan |
10.140.20.45:8080 |
Wake-on-LAN service |
notes.lan |
/home/user/claude/notes |
Static file server (Caddy file_server) |
cockpit.lan |
https://localhost:9090 |
Cockpit, TLS skip verify |
Adding a new service is one file and a reload — the DNS side takes care of itself.
References
- Caddy v2 — reverse proxy with automatic TLS;
tls internalgenerates certs from Caddy's built-in CA - Caddy Cloudsmith apt repo — official packages, more current than Ubuntu's
- dnsmasq —
address=/directive for wildcard DNS answers - systemd-resolved — stub resolver;
Domains=~lanrouting domain syntax - resolvectl(1) —
resolvectl query <name>shows which DNS server answered and from which link - NetworkManager / nmcli —
ipv4.dns-priorityfor per-link DNS priority (positive value = lower priority) - Incus — LXC/VM container manager; containers on
10.140.20.0/24in this setup - Proxmox VE — hypervisor; web UI on port 8006 with self-signed cert
I hope you find this helpful.