How to Set Up Pi-hole in Docker Compose (and Keep It Running)
Step-by-step guide to setting up Pi-hole in Docker Compose — working compose file, port 53 conflicts on Ubuntu, network modes, env vars, and persistent volumes.
If you’ve landed here, you already know what Pi-hole does. Here’s how to set up Pi-hole in Docker Compose — including the port 53 conflict that bites most people on first run, the environment variables that actually matter in v6, and how to make the thing survive container updates without losing your blocklists.
Who This Is For (and Who Should Skip It)
Good fit:
- You’re comfortable with
docker compose up -dand bind mounts - You want Pi-hole as a network-wide DNS filter, pointed at by your router
- You’re on Ubuntu 20.04+ or any Debian-based distro with
systemd-resolvedrunning
Skip this guide if: you want Pi-hole to serve DHCP as well. DHCP needs the container to send Layer 2 broadcasts, which bridge networking won’t pass through. For DHCP you’ll need macvlan or network_mode: host — covered briefly later, but not the focus here.
For pure DNS filtering, bridge mode is the right call.
The Port 53 Problem (Sort This First)
On Ubuntu 20.04 and later, systemd-resolved occupies port 53 on 127.0.0.53. Pi-hole can’t bind there and the container will start but DNS won’t work. Check before you do anything:
sudo ss -tulpn | grep ':53'
If you see systemd-resolve in the output, free the port:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
That last step gives the host a working resolver until Pi-hole is up. Once Pi-hole is running you can change /etc/resolv.conf to point at 127.0.0.1 if you want the host to use it too.
The Compose File
This is the canonical starting point, pulled from the official Pi-hole Docker documentation ↗:
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
- "443:443/tcp"
environment:
TZ: "America/New_York"
FTLCONF_webserver_api_password: "changeme"
FTLCONF_dns_listeningMode: "ALL"
volumes:
- ./etc-pihole:/etc/pihole
cap_add:
- NET_ADMIN
restart: unless-stopped
A few things worth flagging:
FTLCONF_dns_listeningMode: 'ALL' is required on Docker’s default bridge network. Without it, Pi-hole only listens on its loopback interface and silently drops DNS queries coming in from the rest of your LAN. This is the most common cause of “Pi-hole started but nothing is being filtered.”
cap_add: NET_ADMIN is needed for DHCP and some IPv6 operations. Safe to include even if you’re not enabling DHCP — the overhead is negligible.
restart: unless-stopped rather than always. If Pi-hole crashes because port 53 is still occupied, always will restart it in a tight loop. unless-stopped halts after repeated failures and lets you investigate.
Environment Variables That Matter
Pi-hole v6 replaced most configuration knobs with an FTLCONF_* naming scheme. The full variable reference lives at docs.pi-hole.net ↗. The ones you’ll actually touch:
| Variable | Purpose |
|---|---|
TZ | Timezone for log rotation. Use a real TZ string: America/Chicago, Europe/Berlin, etc. |
FTLCONF_webserver_api_password | Web UI password. Omit it and Pi-hole logs a randomly generated one on first start. |
FTLCONF_dns_listeningMode | Set to ALL on bridge networks. Leave it alone if using host or macvlan. |
FTLCONF_dns_upstreams | Upstream resolver(s), semicolon-separated. Defaults to Cloudflare and Google if unset. |
FTLCONF_dns_dnssec | Set to true to enable DNSSEC validation upstream. |
To use a specific upstream resolver:
FTLCONF_dns_upstreams: "1.1.1.1;8.8.8.8"
Or a local Unbound instance running on the same host:
FTLCONF_dns_upstreams: "127.0.0.1#5335"
The #port suffix tells Pi-hole’s FTL to query on a non-standard port, which is how you chain Pi-hole with Unbound for recursive DNS without leaking queries to a third party.
Volumes and What They Actually Persist
The one volume mount that matters:
./etc-pihole:/etc/pihole
This persists Pi-hole’s gravity database (gravity.db), custom DNS entries, allowlists, and the main configuration file. Without it, every container update wipes your blocklists, allowlist entries, and local DNS records — which is a miserable way to discover you hadn’t persisted anything.
The ./etc-dnsmasq.d:/etc/dnsmasq.d mount you’ll see in older compose examples is only needed if you’re migrating from Pi-hole v5 or maintaining custom dnsmasq configs. Fresh v6 installs don’t need it.
Networking: Bridge vs Macvlan
Bridge (default): The compose file above uses this. Pi-hole gets a container IP, and you point your router’s DHCP “DNS server” field at the host machine’s LAN IP. Every device on the network routes DNS through Pi-hole without any per-device configuration. This is the setup that works for 90% of homelabs.
Macvlan: Pi-hole gets its own IP address on the LAN subnet, appearing as a separate network device. Useful when you want Pi-hole to serve DHCP (it needs to send broadcasts), or when your router’s firmware won’t let you set a custom DNS IP. The tradeoff: the Docker host cannot directly reach the macvlan container. You need a macvlan shim interface (ip link add macvlan0 link eth0 type macvlan mode bridge) on the host to communicate with it. More setup, more to debug.
Host mode (network_mode: host) works too and sidesteps the port 53 dance, but it removes all network isolation — the container shares the host’s entire network stack. Reasonable for a dedicated Pi-hole box; less ideal on a host running other containers.
Starting Pi-hole and Verifying It Works
docker compose up -d
docker compose logs -f pihole
Wait for a line containing Pi-hole FTL is listening in the logs, then open http://<host-ip>/admin. Log in with your configured password.
First run is slower — Pi-hole downloads and builds its initial gravity database (blocklists). Progress shows in the web UI under “Gravity” or in the container logs.
To test DNS resolution from the host:
dig @127.0.0.1 ads.google.com
A blocked domain should return 0.0.0.0 or NXDOMAIN. An unblocked domain should return the real IP.
Updating Without Data Loss
docker compose pull
docker compose up -d --remove-orphans
Pi-hole’s built-in cron (baked into the image) refreshes blocklists weekly automatically, so you don’t need to run gravity updates manually. The pull + up -d cycle above handles image updates; your data persists because you’re using named volumes.
If you want upgrade predictability in a setup where DNS downtime matters, pin to a specific version tag from the pihole/pihole Docker Hub page ↗ instead of latest, then bump the tag deliberately when you’re ready.
DNS Filtering as a Security Layer
Pi-hole isn’t just about skipping ads. DNS-layer filtering is one of the cheapest security controls you can run at home — blocking malware C2 domains, phishing infrastructure, and telemetry endpoints before a connection even starts. For broader context on how DNS-layer controls fit into layered home network defenses, Techsentinel News ↗ covers network security developments worth tracking alongside this kind of setup.
Sources
- Pi-hole Docker Documentation ↗ — Official quick-start compose reference from the Pi-hole team, including capability requirements and port mapping notes.
- Pi-hole Docker Configuration Reference ↗ — Complete
FTLCONF_*variable list, array syntax for multi-value settings, and advanced FTL options. - pi-hole/docker-pi-hole on GitHub ↗ — Official image source and release notes; useful for tracking version-to-version config changes.
- pihole/pihole on Docker Hub ↗ — Image tags and pull stats; check here when pinning to a specific version.
Sources
Related
Docker Compose Networking: Bridge, Host, and Custom Networks
Understand Docker Compose networking — how containers find each other, how to isolate services, and how to expose only what needs exposing.
Best Docker Containers for Your Home Server in 2026
A practical homelab operator's guide to the best docker containers for home server use — Jellyfin, Vaultwarden, Nextcloud, Tailscale, and a dozen more
Self-Host Immich (Photos) with Docker Compose
Replace Google Photos with Immich, a self-hosted photo and video backup server. The official Docker Compose stack, the .