Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/damianiglesias/pihole-ubuntu-deploy/llms.txt

Use this file to discover all available pages before exploring further.

The Ansible method deploys Pi-hole and Unbound as Docker containers using the install_pihole.yml playbook. It is the recommended approach for virtual machines and any environment where you want repeatable, idempotent infrastructure. Pi-hole and Unbound run side-by-side on a custom Docker bridge network (172.20.0.0/16) — Pi-hole at 172.20.0.2 and Unbound at 172.20.0.5 — with Pi-hole forwarding all DNS queries to Unbound on port 5335.
The pihole_password variable defaults to 1234. Change this value before running the playbook. See Variables below.

Prerequisites

  • Ubuntu Server 20.04, 22.04, or 24.04
  • Ansible installed on the machine you run the playbook from (the playbook installs Docker itself on the target host)
Install Ansible if you do not have it yet:
sudo apt update && sudo apt install ansible -y

Installation Steps

1

Clone the repository

Download the project and navigate to the Ansible directory:
git clone https://github.com/damianiglesias/pihole-ubuntu-deploy && cd pihole-ubuntu-deploy/ansible
2

Install Ansible

If you have not already installed Ansible on your control machine:
sudo apt update && sudo apt install ansible -y
3

(Optional) Edit the inventory

Open ansible/inventory.ini. By default it targets localhost over a local connection — no SSH required:
[pihole_server]
localhost ansible_connection=local
To deploy to a remote host, replace the entry with the target’s address and credentials. See Inventory below for details.
4

(Optional) Set your admin password

Open install_pihole.yml and change the pihole_password variable from the insecure default:
vars:
  project_dir: "/root/pihole-docker"
  pihole_password: "your-secure-password"   # change this
5

Run the playbook

Execute the playbook against your inventory:
ansible-playbook -i inventory.ini install_pihole.yml
Ansible will run all ten tasks in sequence and print a play recap when finished. Pi-hole’s web interface will be reachable at http://<server-ip>/admin.

What the Playbook Does

The playbook contains ten ordered tasks. Each is idempotent and safe to re-run.
1

Task 1 — Stop and disable native services

Stops and disables any previously installed native instances of the following services to free up ports 53, 80, and 5335. Errors are ignored so the playbook succeeds on fresh machines where these services were never installed.
  • pihole-FTL
  • unbound
  • lighttpd
  • systemd-resolved
2

Task 2 — Fix DNS resolution for Docker

Sets DNSStubListener=no in /etc/systemd/resolved.conf. This prevents systemd-resolved from occupying port 53 even when it is restarted later, allowing the Pi-hole container to bind to that port correctly. systemd-resolved is then restarted via an Ansible handler (Restart systemd-resolved) to apply the change.
3

Task 3 — Write a generic resolv.conf

Overwrites /etc/resolv.conf with a minimal, known-good nameserver so the host can resolve DNS during the rest of the playbook:
nameserver 8.8.8.8
4

Task 4 — Remove conflicting Docker packages

Purges any existing Docker CE packages that may conflict with the docker.io package from Ubuntu’s official repos:
  • docker-ce
  • docker-ce-cli
  • containerd.io
  • docker-compose-plugin
5

Task 5 — Install dependencies

Installs required packages via apt with update_cache: yes:
  • curl
  • net-tools
  • docker.io
  • docker-compose-v2
6

Task 6 — Create project directory

Creates the directory that will hold the Docker Compose file and Pi-hole data volumes. Defaults to /root/pihole-docker (controlled by the project_dir variable).
mkdir -p /root/pihole-docker   # mode 0755
7

Task 7 — Write the Docker Compose file

Renders and writes docker-compose.yml to {{ project_dir }}. See Docker Compose Config below for the full generated file.
8

Task 8 — Launch containers

Starts both containers in detached mode:
docker compose up -d
This command runs from inside {{ project_dir }}.
9

Task 9 — Configure UFW firewall rules

Opens the three ports required by Pi-hole (TCP protocol for all):
PortPurpose
22SSH access
80Pi-hole web interface
53DNS queries
10

Task 10 — Enable UFW

Enables the UFW firewall, activating all rules added in Task 9.

Docker Compose Config

The following docker-compose.yml is generated by the playbook and written to {{ project_dir }}/docker-compose.yml:
services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: 'Europe/Madrid'
      WEBPASSWORD: '<pihole_password>'
      PIHOLE_DNS_1: '172.20.0.5#5335'
      PIHOLE_DNS_2: 'no'
    networks:
      pihole_net:
        ipv4_address: 172.20.0.2
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped

  unbound:
    container_name: unbound
    image: mvance/unbound:latest
    networks:
      pihole_net:
        ipv4_address: 172.20.0.5
    ports:
      - "5335:5335/udp"
    restart: unless-stopped

networks:
  pihole_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
The timezone is hardcoded to Europe/Madrid in the TZ environment variable. To change it, edit the TZ value under the pihole service’s environment block in install_pihole.yml before running the playbook.
Key networking details:
ContainerImageIP AddressExposed Port
piholepihole/pihole:latest172.20.0.253/tcp, 53/udp, 80/tcp
unboundmvance/unbound:latest172.20.0.55335/udp
Pi-hole is configured to forward DNS queries to Unbound at 172.20.0.5#5335 (PIHOLE_DNS_1). The second upstream DNS server is set to no (PIHOLE_DNS_2) so all queries are routed exclusively through Unbound. The custom bridge network pihole_net uses the 172.20.0.0/16 subnet to give both containers stable, predictable addresses.

Inventory

The default ansible/inventory.ini targets localhost using a local connection (no SSH):
[pihole_server]
localhost ansible_connection=local
To deploy to a remote Ubuntu server, replace the localhost line with your target host’s details:
[pihole_server]
192.168.1.50 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa
FieldDescription
ansible_hostIP or hostname of the target server (can be set inline as the first token)
ansible_userSSH user to connect as (e.g. ubuntu, root)
ansible_ssh_private_key_filePath to the SSH private key for authentication

Variables

project_dir
string
default:"/root/pihole-docker"
Absolute path on the target host where the Docker Compose file and Pi-hole data volumes (etc-pihole/, etc-dnsmasq.d/) are stored. The directory is created by Task 6 if it does not exist.
pihole_password
string
default:"1234"
Password for the Pi-hole web admin interface. This value is injected into the WEBPASSWORD environment variable of the pihole container. Always change this from the default before running the playbook.

Build docs developers (and LLMs) love