Pi-hole Docker Setup: Block Ads Network-Wide on a Cloud VPS
Learn how to set up Pi-hole on your VPS using Docker Compose to block ads network-wide and secure DNS queries.
Deploying Pi-hole on a Virtual Private Server (VPS) allows you to establish a centralized, cloud-based DNS sinkhole for network-wide ad blocking and tracker prevention across all your devices, regardless of your physical network location. This guide details a production-ready deployment of Pi-hole utilizing Docker Compose, resolving potential port conflicts, and securing the server to prevent it from becoming an open resolver.
Core Docker Compose Configuration
Create a dedicated directory for your Pi-hole deployment and set up the docker-compose.yml file.
mkdir -p ~/pihole
cd ~/pihole
touch docker-compose.yml
Below is the highly technical and optimized docker-compose.yml configuration:
version: "3"
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
# Required for container network administration capabilities (e.g., ARP, DHCP if used)
cap_add:
- NET_ADMIN
ports:
- "53:53/tcp"
- "53:53/udp"
# Map Web UI to port 8080 on the host to avoid conflicting with other web servers (e.g., Nginx)
- "8080:80/tcp"
environment:
TZ: 'UTC'
# Define your admin web UI password here
WEBPASSWORD: 'ChooseAStrongPasswordHere'
# Define upstream DNS servers (e.g., Cloudflare and Quad9)
PIHOLE_DNS_: '1.1.1.1;9.9.9.9'
# Set custom IPv4 of your VPS to properly redirect blocked domains
FTLCONF_LOCAL_IPV4: 'YOUR_VPS_PUBLIC_IP'
# Standard container runtime configurations
DNSMASQ_USER: 'root'
volumes:
- './etc-pihole:/etc/pihole'
- './etc-dnsmasq.d:/etc/dnsmasq.d'
restart: unless-stopped
Key Environment Variables Explained
cap_add: AddingNET_ADMINallows the Pi-hole container to interact directly with the host network stack for lower-level configurations and packet routing.WEBPASSWORD: The password required to authenticate to the admin web console (/admin).PIHOLE_DNS_: A semicolon-separated list of upstream DNS resolvers that Pi-hole queries for non-cached, non-blocked domains.FTLCONF_LOCAL_IPV4: Instructs Pi-hole of the server's public IP address, ensuring it points blocked requests to the correct local address or sinkhole loopback.
Resolving Host Port 53 Bind Conflict
By default, many modern Linux distributions (such as Ubuntu 20.04/22.04/24.04 and Debian) run systemd-resolved. This system service binds to port 53 on the loopback interface (127.0.0.53), preventing Docker from mapping port 53 to the host.
To verify if port 53 is in use, execute:
sudo ss -lptun | grep ":53"
If systemd-resolved is listening, disable its DNS stub listener using the following steps:
- Edit
/etc/systemd/resolved.conf:
sudo nano /etc/systemd/resolved.conf
- Add or modify the
DNSStubListenerdirective tono:
[Resolve]
DNSStubListener=no
- Create a symbolic link pointing
/etc/resolv.confto the runtime resolv.conf managed by systemd-resolved:
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
- Restart the
systemd-resolvedservice to apply the configuration:
sudo systemctl restart systemd-resolved
- Confirm that port 53 is no longer bound:
sudo ss -lptun | grep ":53"
Securing Your Cloud DNS Server
[!WARNING] Running a DNS server with port 53 open to the public internet without access control is highly dangerous. It will be scanned and weaponized as a vector for DNS Amplification DDoS attacks, which can result in your VPS hosting provider suspending your account.
You must implement one of the following security strategies.
Option A: Restrict Access via UFW (Firewall)
If you have static IP addresses at home, work, or other locations from which you intend to use the Pi-hole, restrict port 53 traffic to those specific IPs using the Uncomplicated Firewall (UFW).
# Allow SSH access first to prevent lockouts
sudo ufw allow 22/tcp
# Block all incoming DNS requests by default
sudo ufw default deny incoming
# Allow specific public IPs to query DNS
sudo ufw allow from YOUR_HOME_PUBLIC_IP to any port 53 proto udp
sudo ufw allow from YOUR_HOME_PUBLIC_IP to any port 53 proto tcp
# Allow access to the Web Admin UI (Port 8080)
sudo ufw allow from YOUR_HOME_PUBLIC_IP to any port 8080 proto tcp
# Enable the firewall
sudo ufw enable
Option B: Route DNS through a WireGuard VPN (Highly Recommended)
Instead of exposing port 53 to the public internet, deploy a VPN on the same VPS.
- Bind the Pi-hole container ports only to the WireGuard interface (typically
10.8.0.1or10.0.0.1). - Update the port mapping in
docker-compose.ymlto bind specifically to the private network interface:
ports:
- "10.8.0.1:53:53/tcp"
- "10.8.0.1:53:53/udp"
- "10.8.0.1:8080:80/tcp"
- Set the Pi-hole environment configuration
INTERFACE=wg0or adjust the setting in the Web UI to "Permit only local requests".
Spinning Up the Deployment
With the configurations complete, launch your Docker Compose deployment:
docker compose up -d
Monitor the deployment process and verify container health:
docker compose logs -f pihole
You should see a series of log lines indicating that FTL (Faster-Than-Light) engine has started successfully and the gravity list database has been initialized.
Accessing the Web Admin Console
Once the container reports a healthy status:
- Open your web browser and navigate to:
http://<YOUR_VPS_PUBLIC_IP>:8080/admin(orhttp://10.8.0.1:8080/adminif using the WireGuard VPN setup). - Authenticate using the password specified in the
WEBPASSWORDenvironment variable in yourdocker-compose.yml. - From the dashboard, you can monitor query stats, customize your ad lists (Adlists), and manage white/blacklists.