Install

# Debian / Ubuntu
sudo apt install dnsmasq

# On systems where systemd-resolved listens on :53, you'll conflict; disable resolved's stub:
sudo nano /etc/systemd/resolved.conf
# DNSStubListener=no
sudo systemctl restart systemd-resolved
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

Or just run dnsmasq on the router / dedicated DHCP box, not on a desktop with resolved (see that tutorial).

The minimum-viable config

# /etc/dnsmasq.conf

# DNS: upstream servers for queries dnsmasq can't answer locally
server=1.1.1.1
server=9.9.9.9

# Don't read /etc/resolv.conf (we set servers explicitly)
no-resolv

# Bind only to the LAN interface, never the WAN
interface=eth0
bind-interfaces

# Cache size (entries)
cache-size=10000

# Don't forward names without a dot (single-label names like "router")
domain-needed

# Don't forward reverse lookups for RFC1918 addresses upstream
bogus-priv

# Local domain
domain=lab.example.com
local=/lab.example.com/
expand-hosts

# DHCP — range + lease time
dhcp-range=192.168.1.100,192.168.1.200,12h

# DHCP options pushed to clients
dhcp-option=3,192.168.1.1                  # default gateway
dhcp-option=6,192.168.1.1                  # DNS = this dnsmasq itself
dhcp-option=15,lab.example.com             # domain name

# Log DHCP transactions (for debugging)
log-dhcp
log-facility=/var/log/dnsmasq.log
sudo systemctl restart dnsmasq
sudo systemctl status dnsmasq
journalctl -u dnsmasq -f

DHCP reservations: stable IPs for specific MACs

# In dnsmasq.conf or /etc/dnsmasq.d/reservations.conf

# Reserve an IP for a specific MAC + assign a hostname
dhcp-host=aa:bb:cc:dd:ee:ff,192.168.1.10,nas,infinite
dhcp-host=11:22:33:44:55:66,192.168.1.20,printer,7d

# Reserve by hostname only (IP from pool, no specific IP)
dhcp-host=laptop-amir

The hostname assigned here also becomes resolvable in the local domain — nas.lab.example.com auto-resolves to 192.168.1.10 as long as expand-hosts + domain= are set.

Local DNS records

Three ways to define local names:

  1. /etc/hosts — dnsmasq reads it by default. Add static names:
    # /etc/hosts
    192.168.1.10  nas nas.lab.example.com
    192.168.1.30  proxmox proxmox.lab.example.com
  2. address= directive — wildcard / pattern resolution:
    address=/.lab.example.com/192.168.1.30      # every *.lab.example.com -> .30
    address=/ads.googletagservices.com/0.0.0.0  # null-route (basic ad block)
  3. host-record — explicit A + PTR pairs with options for TTL:
    host-record=internal.lab.example.com,192.168.1.50,3600

Conditional forwarding: query specific zones to specific resolvers

# Send all .corp queries to the corporate DNS server
server=/corp.example.com/10.0.5.1

# Send all .lab queries locally; never forward upstream
local=/lab.example.com/

# Send DNS-over-TLS via stubby / cloudflared / unbound on localhost
server=127.0.0.1#5053

The server=/zone/<ip> directive overrides the default upstream for that zone only.

DNS-level ad blocking

# Drop a hosts-format blocklist in
# /etc/dnsmasq.d/blocklist.conf
# (e.g. from someonewhocares.org/hosts/)
addn-hosts=/etc/dnsmasq.d/blocklist.hosts

Or use Pi-hole, which is essentially dnsmasq's FTL fork with a web UI for managing blocklists. For homelab setups where you want both DHCP + DNS + blocking from one box, Pi-hole is the polished package; for "just DNS + DHCP for my LAN," plain dnsmasq is lighter.

PXE booting

dnsmasq's TFTP + PXE support makes it the easiest way to net-boot machines for diskless OS installs:

enable-tftp
tftp-root=/srv/tftp

# Hand out PXE info
dhcp-boot=pxelinux.0
dhcp-match=set:efi-x86_64,option:client-arch,7
dhcp-boot=tag:efi-x86_64,bootnetx64.efi

Drop the bootloader binaries in /srv/tftp/; install media (Debian / Ubuntu / iPXE) in the right paths; the BIOS-PXE handshake works. Used to flash dozens of identical homelab nodes without USB keys.

The "captive portal" hijack pattern

# Redirect all DNS queries to a specific IP (e.g. a landing page)
address=/#/10.0.5.10

Match-all (/#/) forces every DNS query to resolve to one IP. Useful for the captive-portal "before you log in, every site goes to the portal" trick. Don't deploy on a regular network — it's wholesale DNS hijacking.

IPv6 RA / DHCPv6

# IPv6 stateless auto-config (RA only, clients pick their own addresses)
dhcp-range=fd00:abcd:1234::,ra-only

# Stateful DHCPv6 (dnsmasq assigns specific IPv6 addresses)
dhcp-range=fd00:abcd:1234::100,fd00:abcd:1234::200,64,12h

# Advertise this router as the IPv6 default
ra-param=eth0,10,300

# Push DNS option to clients
dhcp-option=option6:dns-server,fd00:abcd:1234::1

Reload + inspect

# Send HUP to reload (re-reads /etc/hosts, blocklists, leases)
sudo systemctl reload dnsmasq

# Active leases
cat /var/lib/misc/dnsmasq.leases
# Format: expiry-epoch  MAC  IP  hostname  client-id

# Stats
sudo killall -USR1 dnsmasq
journalctl -u dnsmasq | tail -50      # prints cache stats

When dnsmasq isn't the right tool

  • Authoritative DNS for public zones. Use PowerDNS (see that tutorial) or Knot DNS — dnsmasq isn't authoritative.
  • Multi-VLAN / enterprise DHCP with elaborate policies. ISC Kea (the modern replacement for ISC DHCP) has classes, hooks, and HA. dnsmasq is single-server.
  • Recursive resolver with DNSSEC validation. dnsmasq can validate DNSSEC but Unbound is the standard recursive resolver; better caching strategies for high-traffic.
  • Very large LANs (1000+ DHCP clients). dnsmasq handles a few hundred fine; beyond that, dedicated DHCP servers scale better.

Worth knowing

  • The one binary really does everything. Each feature is a flag; you can run a DNS-only or DHCP-only dnsmasq, no need to enable both.
  • Configs in /etc/dnsmasq.d/ — drop separate files; the main config has conf-dir=/etc/dnsmasq.d by default. Cleaner than one big file.
  • The leases file is the truth. Restoring dnsmasq state across boxes means copying /var/lib/misc/dnsmasq.leases; otherwise newly-booted clients get fresh leases.