Port 53 Already in Use: Freeing It from systemd-resolved

Fix "port 53 already in use" on Ubuntu and Debian: diagnose systemd-resolved's stub listener, free the port safely, or redirect DNS traffic with iptables.

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.

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:

ListenerWhere it comes fromWhat to do
dnsmasqOften pulled in by libvirt or LXD for their virtual bridges, sometimes installed standaloneIf 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
namedAn existing BIND9 installStop and disable it, or reconcile the two servers — running both on one host rarely ends well
unboundA prior resolver setupSame: 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.

Frequently asked questions

Is it safe to disable systemd-resolved's stub listener?
Yes, provided /etc/resolv.conf points at a working resolver afterwards. The stub on 127.0.0.53 is only a local proxy; systemd-resolved keeps resolving for the host without it. Repoint the /etc/resolv.conf symlink to /run/systemd/resolve/resolv.conf before restarting the service, or the host loses DNS.
Why can't my Docker container bind port 53?
Ports below 1024 are privileged on Linux. A container running as a non-root user without the CAP_NET_BIND_SERVICE capability gets 'permission denied' on port 53 even when the port is free. Run the container as root, grant the capability, or listen on a high port and redirect port 53 to it with iptables.
Does Docker's embedded DNS occupy port 53 on the host?
No. Docker's embedded DNS server at 127.0.0.11 exists only inside the network namespace of containers on user-defined networks. It never binds port 53 on the host, so it is never the cause of a 'port 53 already in use' error during deployment.