17 July 2026

Caddy and dnsmasq: HTTPS .lan Hostnames for Every Home Lab Service

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:

  1. dnsmasq at 127.0.0.2 — resolves every *.lan name to 127.0.0.1 via a single wildcard rule
  2. systemd-resolved — routes .lan queries to dnsmasq, everything else upstream
  3. 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 internal generates certs from Caddy's built-in CA
  • Caddy Cloudsmith apt repo — official packages, more current than Ubuntu's
  • dnsmasqaddress=/ directive for wildcard DNS answers
  • systemd-resolved — stub resolver; Domains=~lan routing domain syntax
  • resolvectl(1)resolvectl query <name> shows which DNS server answered and from which link
  • NetworkManager / nmcliipv4.dns-priority for per-link DNS priority (positive value = lower priority)
  • Incus — LXC/VM container manager; containers on 10.140.20.0/24 in this setup
  • Proxmox VE — hypervisor; web UI on port 8006 with self-signed cert

I hope you find this helpful.

No comments:

Post a Comment