Install Pi-hole on Ubuntu Server
The Ubuntu Problem
Installing Pi-hole on Ubuntu has one specific pain point that does not exist on other distributions: systemd-resolved. Ubuntu runs a local DNS stub resolver that binds to port 53 by default. Pi-hole also needs port 53. They cannot both have it.
This conflict is the single most common reason Pi-hole fails to start on Ubuntu. This guide walks through resolving it properly, along with Ubuntu-specific networking and firewall configuration.
For the general Pi-hole Docker setup and configuration, see the main Pi-hole guide.
Prerequisites
- Ubuntu Server 22.04 LTS or 24.04 LTS
- Docker and Docker Compose installed (guide)
- 512 MB of free RAM
- 1 GB of free disk space
- A static IP address (configured below)
- Root or sudo access
Platform Setup
Step 1: Disable systemd-resolved
This is the critical Ubuntu-specific step. systemd-resolved runs a DNS stub listener on 127.0.0.53:53. Pi-hole needs to bind to *:53 for DNS. They conflict.
Verify the conflict exists:
sudo ss -tlnp | grep ':53'
If you see systemd-resolve listening on port 53, proceed with the fix.
Option A: Disable the stub listener only (recommended)
This preserves systemd-resolved for the host’s own DNS resolution but frees port 53 for Pi-hole:
# Edit resolved configuration
sudo nano /etc/systemd/resolved.conf
Set these values under [Resolve]:
[Resolve]
DNSStubListener=no
DNS=1.1.1.1
FallbackDNS=8.8.8.8
Update the resolv.conf symlink to use the full resolver instead of the stub:
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
Restart systemd-resolved:
sudo systemctl restart systemd-resolved
Verify port 53 is free:
sudo ss -tlnp | grep ':53'
No output means the port is available for Pi-hole.
Option B: Disable systemd-resolved entirely
If you want Pi-hole to handle ALL DNS for the host machine too:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
Create a static /etc/resolv.conf (this will point to Pi-hole once it is running):
sudo rm /etc/resolv.conf
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
With this option, if Pi-hole goes down, the Ubuntu host itself loses DNS resolution. Option A is safer for servers running other services.
Step 2: Configure a Static IP via Netplan
Your entire network depends on Pi-hole’s IP for DNS. If the IP changes, every device loses DNS resolution. A static IP is mandatory.
Ubuntu Server uses Netplan for network configuration. Edit your Netplan config:
ls /etc/netplan/
# Usually: 00-installer-config.yaml or 01-netcfg.yaml
sudo nano /etc/netplan/00-installer-config.yaml
Replace the DHCP configuration with a static IP:
network:
version: 2
renderer: networkd
ethernets:
ens18: # Your interface name — check with: ip link show
dhcp4: false
addresses:
- 192.168.1.10/24 # Your desired static IP
routes:
- to: default
via: 192.168.1.1 # Your router/gateway IP
nameservers:
addresses:
- 1.1.1.1 # Temporary — will use Pi-hole after setup
- 8.8.8.8
Find your interface name with ip link show and your gateway with ip route show default.
Apply the configuration:
sudo netplan apply
Verify:
ip addr show ens18
ip route show default
Step 3: Configure UFW Firewall Rules
Ubuntu’s UFW firewall blocks DNS traffic by default. Allow the ports Pi-hole needs:
# DNS — required for all DNS resolution
sudo ufw allow 53/tcp comment 'Pi-hole DNS TCP'
sudo ufw allow 53/udp comment 'Pi-hole DNS UDP'
# Web admin interface
sudo ufw allow 80/tcp comment 'Pi-hole web UI'
# Optional: HTTPS for the web admin
sudo ufw allow 443/tcp comment 'Pi-hole web UI HTTPS'
If UFW is not active, enable it:
sudo ufw enable
Verify the rules:
sudo ufw status numbered
If you only want Pi-hole accessible from your local network:
sudo ufw allow from 192.168.1.0/24 to any port 53 comment 'Pi-hole DNS local only'
sudo ufw allow from 192.168.1.0/24 to any port 80 comment 'Pi-hole web local only'
Docker Compose Configuration
Create the Pi-hole directory:
sudo mkdir -p /opt/pihole
Create /opt/pihole/docker-compose.yml:
services:
pihole:
container_name: pihole
image: pihole/pihole:2026.02.0
ports:
- "53:53/tcp" # DNS over TCP
- "53:53/udp" # DNS over UDP
- "80:80/tcp" # Web admin interface
- "443:443/tcp" # Web admin interface HTTPS
environment:
TZ: "${TZ}"
FTLCONF_webserver_api_password: "${PIHOLE_PASSWORD}"
FTLCONF_dns_upstreams: "${DNS_UPSTREAMS}"
FTLCONF_dns_listeningMode: "ALL"
volumes:
- ./etc-pihole:/etc/pihole
cap_add:
- NET_ADMIN # Required for DHCP server and network operations
- SYS_TIME # Required for NTP client
- SYS_NICE # Process priority optimization
restart: unless-stopped
Create /opt/pihole/.env:
# Timezone — use your IANA timezone
TZ=America/New_York
# Web interface password — CHANGE THIS
PIHOLE_PASSWORD=change-me-to-a-strong-password
# Upstream DNS servers (semicolon-separated)
# Cloudflare: 1.1.1.1;1.0.0.1
# Google: 8.8.8.8;8.8.4.4
# Quad9: 9.9.9.9;149.112.112.112
DNS_UPSTREAMS=1.1.1.1;1.0.0.1
Start Pi-hole:
cd /opt/pihole
docker compose up -d
Verify it is running and port 53 is bound:
docker compose logs pihole | tail -20
sudo ss -tlnp | grep ':53'
You should see the Docker proxy listening on port 53, not systemd-resolved.
Test DNS resolution through Pi-hole:
dig @127.0.0.1 google.com
You should get an A record response with a low query time.
Access the admin interface at http://192.168.1.10/admin (use your static IP).
Configuration
Point the Host’s DNS to Pi-hole
Now that Pi-hole is running, update the host’s DNS to use it. This makes the Ubuntu server itself benefit from Pi-hole’s ad blocking:
If you used Option A (stub listener disabled):
sudo nano /etc/systemd/resolved.conf
[Resolve]
DNSStubListener=no
DNS=127.0.0.1
FallbackDNS=1.1.1.1
sudo systemctl restart systemd-resolved
If you used Option B (systemd-resolved disabled), /etc/resolv.conf already points to 127.0.0.1.
DHCP Server Setup (Optional)
Pi-hole can act as your network’s DHCP server, replacing your router’s DHCP. This gives Pi-hole direct visibility into device hostnames and ensures every device uses Pi-hole for DNS without configuring the router.
The NET_ADMIN capability in the Docker Compose config enables this. To set it up:
- Disable DHCP on your router first — two DHCP servers on the same network cause IP conflicts
- In the Pi-hole web UI, go to Settings > DHCP
- Enable the DHCP server
- Set the IP range (e.g.,
192.168.1.100to192.168.1.250) - Set the router/gateway IP (e.g.,
192.168.1.1)
For Docker-based Pi-hole to serve DHCP correctly, add network_mode: host to the Docker Compose and remove the ports: section:
services:
pihole:
container_name: pihole
image: pihole/pihole:2026.02.0
network_mode: host
environment:
TZ: "${TZ}"
FTLCONF_webserver_api_password: "${PIHOLE_PASSWORD}"
FTLCONF_dns_upstreams: "${DNS_UPSTREAMS}"
FTLCONF_dns_listeningMode: "ALL"
volumes:
- ./etc-pihole:/etc/pihole
cap_add:
- NET_ADMIN
- SYS_TIME
- SYS_NICE
restart: unless-stopped
With host networking, Pi-hole binds directly to the host’s network interface, which is required for DHCP broadcasts to work properly.
Point Your Router to Pi-hole
If you are not using Pi-hole as a DHCP server, configure your router to use Pi-hole as the DNS server:
- Log into your router’s admin panel
- Find DHCP/DNS settings
- Set primary DNS to
192.168.1.10(your Pi-hole server’s static IP) - Set secondary DNS to the same IP (or leave blank — do not use a non-Pi-hole DNS as secondary)
- Save and reboot the router
Platform-Specific Tips
- Do not reinstall systemd-resolved. Some Ubuntu updates or package installations can re-enable the stub listener. After a major
apt upgrade, verify port 53 is still free:sudo ss -tlnp | grep ':53'. - Ubuntu unattended-upgrades. Automatic security updates can restart system services. This will not affect Pi-hole (it runs in Docker), but could restart
systemd-resolvedif you used Option A. Add a post-upgrade check script if this concerns you. - Docker’s DNS configuration. Docker containers by default use the host’s
/etc/resolv.conffor DNS. After pointing the host’s DNS to Pi-hole (127.0.0.1), all Docker containers on the host automatically use Pi-hole for DNS resolution. - IPv6 DNS. If your network uses IPv6, add the Pi-hole container’s IPv6 address to your router’s DHCPv6 DNS settings. Otherwise, IPv6 DNS queries bypass Pi-hole entirely.
Troubleshooting
”Address Already in Use” on Port 53
Symptom: docker compose up fails with Error starting userland proxy: listen tcp4 0.0.0.0:53: bind: address already in use.
Fix: systemd-resolved is still listening on port 53. Follow Step 1 above to disable the stub listener. Verify with:
sudo ss -tlnp | grep ':53'
If something other than systemd-resolved is using port 53 (like dnsmasq):
sudo systemctl stop dnsmasq
sudo systemctl disable dnsmasq
Host Loses DNS After Pi-hole Container Stops
Symptom: If Pi-hole is down, the Ubuntu host cannot resolve any domain names. apt update, curl, and everything else fails.
Fix: This happens when the host’s DNS is set to 127.0.0.1 (Pi-hole) with no fallback. Two options:
-
Keep a fallback DNS in
systemd-resolved:[Resolve] DNS=127.0.0.1 FallbackDNS=1.1.1.1 -
Or manually set
/etc/resolv.confduring maintenance:echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf # Do maintenance # Then restore: echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
Pi-hole Shows 0 Queries After Router DNS Change
Symptom: Pi-hole dashboard shows 0 queries even though the router’s DNS is set to Pi-hole’s IP.
Fix: Devices use the DNS server assigned by DHCP. After changing the router’s DNS settings, devices will not pick up the change until they renew their DHCP lease.
Force renewal on individual devices by disconnecting/reconnecting to the network. Or wait 24 hours for all leases to cycle. You can also shorten your router’s DHCP lease time to 1 hour to speed this up.
Verify a specific device is using Pi-hole:
# On the client device
nslookup google.com
# Should show your Pi-hole server IP as the DNS server
UFW Blocks DNS After Reboot
Symptom: DNS works immediately after setup but stops after a server reboot.
Fix: UFW rules persist across reboots, but the rule order matters. Verify:
sudo ufw status numbered
Make sure the ALLOW 53 rules are listed before any DENY rules. If you have a default deny policy, the allow rules must be added before enabling the firewall.
Also check that Docker’s iptables integration is not conflicting with UFW. Docker manipulates iptables directly, which can bypass UFW rules. If you see issues, add to /etc/docker/daemon.json:
{
"iptables": false
}
Then restart Docker and manually manage the firewall rules. This is an advanced configuration — most setups work without it.
Slow DNS Resolution
Symptom: Pages take noticeably longer to load with Pi-hole than without.
Fix: Check upstream DNS latency:
dig @1.1.1.1 google.com | grep "Query time"
dig @8.8.8.8 google.com | grep "Query time"
If upstream DNS is slow (>50ms), try a closer upstream server or set up Unbound as a local recursive resolver.
Also check that Pi-hole’s cache is working: the dashboard should show a high cache hit rate (>50%). If cache hits are low, verify FTLCONF_dns_cache_size is not set to 0.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments