Pi-hole in Docker: The Complete Step-by-Step Installation and Hardening Guide
Install Pi-hole with Docker Compose, configure advanced DNS settings, reverse proxy, backups, and solve common issues. Full guide with copy-paste commands.
Introduction
Network-wide ad blocking is the cornerstone of a privacy-respecting home lab. Pi-hole, when deployed via Docker, offers a clean, reproducible, and easily updatable solution that doesn't pollute your host OS. Unlike manual installations that scatter files across your system, a Dockerized Pi-hole encapsulates everything into a single container, making backups trivial and rollbacks instant. This approach also allows you to run Pi-hole alongside other services like Unbound or WireGuard without conflicts.
In this guide, you will learn not just how to run docker run and forget, but how to build a production-grade Pi-hole setup. We will cover the exact Docker Compose file you need, with all volume mappings and environment variables explained. You will also learn how to configure your router to use Pi-hole as the DNS server, set up a reverse proxy for the web interface with SSL, implement automated backups, and harden the container against common security pitfalls.
By the end, you will have a fully functional, secure, and maintainable Pi-hole installation that serves your entire home network. Whether you are a beginner taking your first steps into self-hosting or an experienced homelabber looking for a reference, this guide provides everything in a copy-paste-ready format. No vague instructions, no “etc.”—just precise commands and configurations.
Let’s dive into the world of DNS-level ad blocking with Docker.
Prerequisites / Requirements
Before you begin, ensure your host system meets the following minimum and recommended specifications. The table below breaks down each requirement.
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | 1 core | 2 cores (ARM or x86) | Pi-hole is lightweight; even a Raspberry Pi Zero 2 W works. x86_64, armv7, or arm64 are supported. |
| RAM | 512 MB | 1 GB | The container typically uses 100-200 MB, but the OS and Docker overhead need headroom. |
| Storage | 2 GB free | 10 GB free | Logs and blocklists can grow. Use an SSD for better performance. |
| Software | Docker 20.10+ | Docker 24+ with Compose v2 | Install via official Docker repo. docker-compose standalone is deprecated; use docker compose plugin. |
| Network | Static IP for host | DHCP reservation | You need a fixed IP for the DNS server to be reliable. |
| Router | DNS settings configurable | Any router that allows custom DNS | You must be able to point clients to your Pi-hole IP. |
| Ports | 53/udp, 53/tcp, 80/tcp, 443/tcp | Same | Port 53 must be free on the host. If systemd-resolved uses it, you must disable it. |
Important: On Ubuntu 22.04+, systemd-resolved listens on port 53. You must stop and disable it before starting Pi-hole, or change Pi-hole's port (not recommended for production).
Step-by-Step Installation Guide
Follow these steps in order. Each step includes the exact command or configuration file you need.
Step 1: Prepare the Host System
First, update your package list and install required tools. Then disable systemd-resolved to free port 53.
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git
Now, stop and disable systemd-resolved:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
Edit /etc/resolv.conf to use a public DNS temporarily (so you don't lose connectivity):
sudo rm /etc/resolv.conf
sudo bash -c 'echo "nameserver 1.1.1.1" > /etc/resolv.conf'
Note: This change is temporary and will be overwritten by Pi-hole's container once it runs. Do not skip this step, or Docker will fail to resolve external domains.
Step 2: Install Docker and Docker Compose Plugin
If you don't have Docker installed, use the official convenience script:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Verify the installation and enable the Docker Compose plugin:
sudo docker --version
sudo docker compose version
If docker compose is not recognized, install the plugin manually:
sudo apt install -y docker-compose-plugin
Step 3: Create a Directory Structure for Pi-hole
Create a dedicated directory to keep all Pi-hole files organized. This will make backups and updates easier.
mkdir -p ~/pihole/{etc-pihole,etc-dnsmasq.d}
cd ~/pihole
etc-pihole: Stores Pi-hole's configuration, databases, and blocklists.etc-dnsmasq.d: Stores custom dnsmasq configuration files.
Step 4: Write the Docker Compose File
Create a file named docker-compose.yml in the ~/pihole directory:
nano docker-compose.yml
Paste the following complete configuration. Do not omit any lines; this is the production-ready version.
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
restart: unless-stopped
hostname: pihole
domainname: lab.local
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
- "443:443/tcp"
environment:
TZ: 'America/New_York' # Change to your timezone
WEBPASSWORD: 'your_strong_password' # Set a strong password
FTLCONF_LOCAL_IPV4: '192.168.1.100' # Your host's static IP
REV_SERVER: 'false'
PIHOLE_DNS_: '1.1.1.1;1.0.0.1' # Upstream DNS servers
DNSSEC: 'true'
DNSMASQ_LISTENING: 'all' # Listen on all interfaces
CONDITIONAL_FORWARDING: 'false'
volumes:
- './etc-pihole:/etc/pihole'
- './etc-dnsmasq.d:/etc/dnsmasq.d'
cap_add:
- NET_ADMIN # Required for DNS and DHCP
dns:
- 127.0.0.1 # Prevent Docker's internal DNS from interfering
- 1.1.1.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/admin/" ]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
networks:
- pihole_net
networks:
pihole_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
Explanation of key variables:
WEBPASSWORD: Set a strong password for the web interface. If empty, a random password is generated and shown in the logs.FTLCONF_LOCAL_IPV4: Must match your host's IP address. This prevents Pi-hole from guessing incorrectly.PIHOLE_DNS_: Upstream DNS resolvers. Use a semicolon to separate multiple servers.DNSSEC: Enables DNS Security Extensions for better security.cap_add: NET_ADMIN: Gives the container the necessary capability to modify network settings (required for DHCP mode).dns: 127.0.0.1: Prevents Docker's internal DNS from hijacking queries.
Step 5: Start the Pi-hole Container
Run the following command to pull the image and start the container in detached mode:
sudo docker compose up -d
This will download the latest Pi-hole image and start it. Wait for the container to become healthy. Check the status with:
sudo docker compose ps
You should see the pihole service with status running and health healthy. If it's not healthy, wait 30 seconds and check again.
Step 6: Verify Pi-hole is Responding to DNS Queries
Test that Pi-hole is correctly resolving DNS queries. Use dig (install with sudo apt install dnsutils if needed):
dig @127.0.0.1 google.com
You should see a response with an IP address. Also, check the web interface by opening your browser to http://<your-host-ip>/admin/. Log in with the password you set.
Step 7: Configure Your Router to Use Pi-hole
To block ads network-wide, you must change your router's DHCP settings to assign Pi-hole's IP as the DNS server. The exact steps vary by router, but generally:
- Log into your router's admin panel.
- Find the DHCP or LAN settings.
- Set the primary DNS server to your Pi-hole host's IP (e.g.,
192.168.1.100). - Set the secondary DNS to a public resolver like
1.1.1.1as a fallback. - Save and reboot the router or renew DHCP leases on your devices.
After this, all devices on your network will use Pi-hole for DNS queries. You can verify by checking the “Query Log” in the Pi-hole web interface.
Step 8: Set a Static IP for Your Host (If Not Already Done)
Ensure your Pi-hole host has a static IP address, otherwise its IP may change and break DNS resolution. On Ubuntu, edit the netplan configuration or use your router's DHCP reservation feature.
For a quick netplan example (Ubuntu 22.04):
sudo nano /etc/netplan/01-network-manager-all.yaml
Add your static IP configuration:
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: no
addresses:
- 192.168.1.100/24
routes:
- to: default
via: 192.168.1.1
nameservers:
addresses: [1.1.1.1, 1.0.0.1]
Apply the changes:
sudo netplan apply
Warning: Make sure you use the correct interface name (e.g.,
eth0,ens3). Useip ato check.
Step 9: Update Pi-hole Regularly
To update Pi-hole to the latest version, simply pull the new image and recreate the container:
cd ~/pihole
sudo docker compose pull
sudo docker compose up -d
This will fetch the latest image and restart the container with the same configuration. Your data persists due to the volume mounts.
Step 10: Configure Web Interface Access (Optional but Recommended)
By default, the web interface is available on port 80. To avoid conflicts with other services, you can change the port in the compose file (e.g., 8080:80) but then you must update the FTLCONF_LOCAL_IPV4 to include the port? Actually, the web interface port is independent. If you change the host port, just access http://<host-ip>:8080/admin/. No other changes needed.
For better security, we'll later set up a reverse proxy with SSL.
Advanced Configuration / Optimization
Now that Pi-hole is running, let's harden and optimize it.
Reverse Proxy with SSL (Using Nginx Proxy Manager)
To access Pi-hole securely via HTTPS, set up Nginx Proxy Manager (NPM) as a reverse proxy. This also allows you to use a custom domain name.
First, create a new docker-compose.yml for NPM (or add to the same file, but separate is cleaner). Create a directory ~/nginx-proxy-manager and a compose file:
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: npm
restart: unless-stopped
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "81:81" # Admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- pihole_net
networks:
pihole_net:
external: true
But first, we need to ensure both containers are on the same Docker network. Our Pi-hole compose creates a network named pihole_pihole_net. We'll use that network as external for NPM.
Start NPM:
mkdir -p ~/nginx-proxy-manager/{data,letsencrypt}
cd ~/nginx-proxy-manager
sudo docker compose up -d
Now, in NPM's admin UI (http://:81), add a new proxy host:
- Domain:
pi-hole.lab.local(or your actual domain) - Forward Hostname:
pihole(the container name of Pi-hole) - Forward Port:
80 - Enable SSL: Request a new Let's Encrypt certificate, or use your own.
Once configured, access Pi-hole via https://pi-hole.lab.local.
Backup Strategy
The volumes ./etc-pihole and ./etc-dnsmasq.d contain all critical data. You can back them up with a simple cron job. Create a script ~/pihole/backup.sh:
#!/bin/bash
BACKUP_DIR="/path/to/backup/location"
DATE=$(date +%Y%m%d-%H%M%S)
tar -czf "$BACKUP_DIR/pihole-backup-$DATE.tar.gz" -C ~/pihole etc-pihole etc-dnsmasq.d
# Keep only last 7 backups
find "$BACKUP_DIR" -name "pihole-backup-*" -mtime +7 -delete
Make it executable and add a cron job:
chmod +x ~/pihole/backup.sh
crontab -e
Add the following line to run the backup daily at 3 AM:
0 3 * * * /home/youruser/pihole/backup.sh
Security Hardening
- Do not expose port 53 to the internet: Only allow DNS queries from your local network. Use firewall rules to block external access.
- Use a non-standard web interface port: If you don't use a reverse proxy, change the host port for the web UI to something like 8080 to avoid automated scans.
- Enable DNSSEC: Already set in the compose file. Verify in the Pi-hole settings under DNS.
- Disable the web interface's default admin path: You can use the
VIRTUAL_HOSTenvironment variable to restrict access, but this is complex. Instead, rely on the reverse proxy and strong password. - Run the container as a non-root user: Pi-hole's official image runs as root by default. To harden, you can override the user, but this may break some features. For now, rely on container isolation.
- Update the blocklists regularly: Pi-hole automatically updates lists, but you can also add custom lists from sources like Firebog for extra protection.
Troubleshooting Common Issues
| Error/Symptom | Likely Cause | Solution |
|---|---|---|
| Port 53 already in use | systemd-resolved is running | sudo systemctl stop systemd-resolved and sudo systemctl disable systemd-resolved. Then restart Pi-hole. |
| Container restarts in a loop | Incorrect FTLCONF_LOCAL_IPV4 or volume permissions |
Check the logs with sudo docker compose logs pihole. Ensure the etc-pihole directory is writable by the container (run sudo chown -R 1000:1000 etc-pihole). |
| DNS queries timeout | Firewall blocking port 53 | Allow UDP/TCP 53 on your firewall: sudo ufw allow 53/udp and sudo ufw allow 53/tcp. |
| Web interface not loading | Port 80 conflict with another service | Change the host port in the compose file (e.g., 8080:80) and re-run docker compose up -d. |
| No ads blocked | Clients are not using Pi-hole as DNS | Check your router's DHCP settings. Ensure the DNS is set to your Pi-hole IP. Also, clear the DNS cache on clients. |
| Container exits with code 137 | Out of memory | Increase RAM or add a memory limit to the container. Check dmesg for OOM killer. |
| Cannot pull image | Network issue or proxy | Check internet connectivity and DNS resolution. Try sudo docker pull pihole/pihole:latest manually. |
| Time zone wrong | TZ environment variable not set correctly | Update the TZ variable in the compose file and recreate the container. |
Conclusion and FAQ
You now have a fully functional Pi-hole running in Docker, with a robust configuration, reverse proxy, and backup strategy. This setup will protect your entire network from ads and trackers, while giving you full control over your DNS traffic. The modular nature of Docker makes it easy to update, back up, and even migrate to a new host. Remember to regularly check the Pi-hole dashboard for blocked queries and to update your blocklists for maximum protection.
FAQ
1. Can I run Pi-hole on a Raspberry Pi with Docker?
Yes, Pi-hole is designed to run on ARM devices. The official Docker image supports armv7 and arm64. Just ensure you have Docker installed on your Raspberry Pi OS (64-bit recommended) and follow the same steps. The performance is excellent even on a Pi 3 or 4.
2. How do I change the web interface password?
You can change it by editing the WEBPASSWORD environment variable in your docker-compose.yml and then running sudo docker compose up -d to recreate the container. Alternatively, you can change it from the web interface under Settings > Web Interface, but the environment variable will override it on the next restart.
3. Does Pi-hole block all ads?
Pi-hole blocks ads that are served from known ad domains. It cannot block ads that are embedded in the same domain as the content (e.g., YouTube ads). For those, you need a browser extension or a more advanced solution like a proxy. However, Pi-hole covers the vast majority of banner ads and trackers.
4. Can I use Pi-hole with a VPN for remote ad blocking?
Yes, you can combine Pi-hole with WireGuard or OpenVPN. The VPN server can push Pi-hole's IP as the DNS server to clients, so even when you are away from home, your queries are filtered. Set up the VPN container on the same Docker network as Pi-hole for easy communication.
5. How do I migrate Pi-hole to a new host?
Back up the etc-pihole and etc-dnsmasq.d directories, then copy them to the new host. Install Docker and create the same directory structure. Place the backup files in the correct locations, and start the container with the same docker-compose.yml. Your configuration and query history will be preserved.
Now go ahead and enjoy a cleaner, faster, and more private internet experience with your self-hosted Pi-hole.