failed to bind port 53: address already in use — if you see this while starting Pi-hole, AdGuard Home, EliHole, BIND, or Unbound on Ubuntu or Debian, the culprit is almost always systemd-resolved. It ships enabled by default and holds a stub DNS listener on 127.0.0.53:53, which blocks any other server from binding the wildcard address on that port. This guide shows how to confirm that, then walks through three fixes: disabling the stub, binding to a specific IP, or running your DNS server on a high port with an iptables redirect.
Confirm what’s holding port 53
Don’t guess — ask the kernel which process owns the socket:
sudo ss -lunp 'sport = :53' On a stock Ubuntu server the output looks like this:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=512,fd=16))
UNCONN 0 0 127.0.0.54%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=512,fd=18)) ss -lunp lists listening (-l) UDP (-u) sockets numerically (-n) with the owning process (-p); the sport = :53 filter narrows it to port 53. Check TCP too with -ltnp, since DNS uses TCP for large responses and zone transfers.
If you prefer lsof:
sudo lsof -i :53 The process name shows as systemd-resolve (the kernel truncates it to 15 characters). Recent systemd versions bind a second stub on 127.0.0.54 as well — both belong to the same service. If the listener you find is something else entirely, jump to the other suspects.
Why systemd-resolved sits on the port
systemd-resolved is the default resolver service on Ubuntu and many Debian-based systems. Its DNSStubListener= option defaults to yes, which makes it bind a stub resolver on 127.0.0.53:53 (UDP and TCP). The point of the stub is to give local programs a stable DNS endpoint: /etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf, which contains a single line — nameserver 127.0.0.53 — so every lookup on the host flows through systemd-resolved.
The conflict comes from how socket binding works. The stub holds a specific address on port 53, and the kernel refuses to let another process bind the wildcard 0.0.0.0:53 while any specific address on that port is taken. Your DNS server wants the wildcard; systemd-resolved already owns a slice of it; bind fails.
Three ways out, in order of preference.
Fix A: disable the stub listener (recommended)
Don’t edit /etc/systemd/resolved.conf directly — package upgrades can clobber it. Use a drop-in:
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/disable-stub.conf <<'EOF'
[Resolve]
DNSStubListener=no
EOF Before restarting anything, fix /etc/resolv.conf. It currently points lookups at 127.0.0.53 — the very stub you’re about to remove. Skip this step and the host itself stops resolving: apt fails, git pull fails, and you’re debugging DNS over a console session. Repoint the symlink at the file where systemd-resolved publishes the real upstream servers:
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf Alternatively, replace the symlink with a static file naming your router or a public resolver, such as nameserver 192.168.1.1. Now restart:
sudo systemctl restart systemd-resolved Verify the port is free and the host still resolves:
sudo ss -lunp 'sport = :53' # should print only the header
resolvectl query example.com # should still answer systemd-resolved keeps running and keeps managing per-link DNS from DHCP — it just no longer occupies port 53. Your DNS server can now bind it, subject to one caveat: a process binding a privileged port still needs root or CAP_NET_BIND_SERVICE, which matters for containers (see Fix C).
Fix B: bind to a specific interface IP
If you’d rather leave systemd-resolved untouched, exploit the fact that the stub binds only loopback addresses. Configure your DNS server to listen on the machine’s LAN IP — say 192.168.1.10 — instead of 0.0.0.0. Two different specific addresses on the same port don’t conflict, so both servers coexist: systemd-resolved answers the host on 127.0.0.53, your sinkhole answers the network on 192.168.1.10.
In Pi-hole this is the “Bind only to interface” setting; in AdGuard Home and Unbound it’s the listen-address option; with Docker, publish the port as 192.168.1.10:53:53/udp instead of 53:53/udp.
The trade-off: your server’s config is now tied to one IP. If the host gets its address from DHCP and it changes, DNS silently breaks — pin a static IP or a DHCP reservation first.
Fix C: high port plus iptables redirect
The third option sidesteps the privileged-port problem entirely: run the DNS server on an unprivileged port and have the kernel translate incoming port 53 traffic to it. This is EliHole’s default deployment path — the container runs as a non-root user, and a process without CAP_NET_BIND_SERVICE can’t bind anything below 1024 no matter how free port 53 is, so EliHole listens on UDP 5354 and one NAT rule does the rest:
sudo iptables -t nat -A PREROUTING -p udp --dport 53 ! -s 172.16.0.0/12 -j REDIRECT --to-port 5354 The ! -s 172.16.0.0/12 exclusion keeps traffic originating from Docker’s own networks out of the redirect. Persist the rule across reboots:
sudo apt install iptables-persistent -y
sudo netfilter-persistent save Two things to know about this approach. First, PREROUTING only sees packets arriving from the network — queries the host sends to itself bypass it, so test from another machine (dig @192.168.1.10 example.com) or query the high port directly (dig @127.0.0.1 -p 5354 example.com). Second, it composes cleanly with Fix A or Fix B: the stub on 127.0.0.53 never conflicts with a server on 5354, so you can keep systemd-resolved exactly as it is.
When it isn’t systemd-resolved
If ss names a different process, work down this list:
| Listener | Where it comes from | What to do |
|---|---|---|
dnsmasq | Often pulled in by libvirt or LXD for their virtual bridges, sometimes installed standalone | If it serves a bridge (e.g. 192.168.122.1), leave it and use Fix B; if standalone, set port=0 or remove the package |
named | An existing BIND9 install | Stop and disable it, or reconcile the two servers — running both on one host rarely ends well |
unbound | A prior resolver setup | Same: disable it or move it to another port/IP |
One non-suspect worth naming: Docker’s embedded DNS. Inside containers on user-defined networks, /etc/resolv.conf points at 127.0.0.11 — Docker’s internal resolver. That address exists only inside each container’s network namespace and never binds port 53 on the host. A surprising number of troubleshooting threads send people chasing it; if ss on the host doesn’t show it, it isn’t your problem.
Port 53 is yours — now make it stay up
Once your sinkhole binds port 53 (or answers it via redirect), every device on the network depends on that single socket. The next question isn’t whether DNS works — it’s what happens when that one server reboots, loses power, or dies mid-upgrade, and the answer is covered in high-availability options for self-hosted DNS.