Skip to main content

Command Palette

Search for a command to run...

Run Pi-hole on a Raspberry Pi with Docker Compose

Updated
View as Markdown
Run Pi-hole on a Raspberry Pi with Docker Compose

Pi-hole is a DNS sinkhole that blocks ads and trackers network-wide, without relying on browser extensions. Running it as a Docker Compose service on a Raspberry Pi keeps the setup self-contained and easy to update — pull a new image, recreate the container, done. This tutorial assumes Docker and Docker Compose are already installed and covers only the Pi-hole side of the setup.

Prerequisites

  • A Raspberry Pi with Docker and Docker Compose already installed — see Install Docker and Docker Compose on a Raspberry Pi if you haven't set that up yet

  • A static IP (or DHCP reservation) assigned to the Raspberry Pi — every client on the network will point to this IP as its DNS server

  • An active internet connection

Callout — Static IP first If the Raspberry Pi's IP changes after a reboot, every device on the network loses DNS resolution. Set up a static IP (via dhcpcd.conf or a DHCP reservation on the router) before proceeding.

How this setup works

Pi-hole normally binds to port 53 (DNS) and port 80 (admin web UI) on the host. When you containerize it, the cleanest way to preserve that behavior is to run the container with network_mode: host — the container shares the Raspberry Pi's network stack directly, so Pi-hole answers DNS queries on the Pi's own IP exactly as if it weren't containerized. The alternative, bridge networking with explicit port mapping, works too, but UDP port mapping for DNS is fiddlier to get right and adds a layer of NAT you don't need on a dedicated DNS box — host networking is what most Pi-hole-in-Docker setups use in practice.

On Raspberry Pi OS there's usually nothing else already listening on port 53 (unlike Ubuntu, where systemd-resolved often claims it), but it's still worth a quick check before starting the container.

Steps

1. Confirm port 53 is free

sudo ss -tulpn | grep ':53'

If this returns nothing, you're clear to proceed. (You may see unrelated entries on port 5353 from avahi-daemon — that's mDNS/.local hostname resolution, a completely different port that never conflicts with Pi-hole's DNS on port 53.)

2. Create a project directory and compose file

mkdir -p ~/pihole/etc-pihole ~/pihole/etc-dnsmasq.d
cd ~/pihole

Create docker-compose.yml:

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    network_mode: host
    environment:
      TZ: 'Europe/Rome'
      FTLCONF_webserver_api_password: 'change-me'
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    cap_add:
      - NET_ADMIN
    restart: unless-stopped

Adjust TZ to your timezone and replace change-me with a real password for the admin panel.

Callout — Environment variable names Recent Pi-hole images use the FTLCONF_* naming scheme for configuration environment variables, replacing the older WEBPASSWORD variable from earlier image versions. Check the tag you're pulling against the image's documentation if you're using anything other than latest.

3. Start the container

docker compose up -d

Docker pulls the pihole/pihole image and starts the container in the background, bound to the host network. Since it's created with restart: unless-stopped, it will come back up on its own after any reboot or power cycle, as long as Docker itself is enabled at boot.

4. Point devices at Pi-hole

For the blocking to apply network-wide, set the Raspberry Pi's IP as the primary DNS server on your router (preferred — applies to every device automatically), or configure it individually in each device's network settings.

5. Verify it's working

Open the admin panel from a browser:

http://<raspberry-pi-ip>/admin

Log in with the password you set in the compose file. The dashboard should start showing DNS queries from configured devices, along with a percentage of blocked requests.

Summary

We created a Docker Compose service for Pi-hole on a Raspberry Pi, using host networking so it binds directly to ports 53 and 80 exactly as a native install would, with persistent volumes for its configuration. Combined with Docker's own auto-start on boot, this gives you a network-wide ad blocker that comes back up on its own after every power cycle.