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.

ansible/install_pihole.yml deploys Pi-hole and Unbound as Docker containers on an Ubuntu target host. The playbook is idempotent and uses become: yes for privilege escalation throughout all tasks.

Playbook Metadata

FieldValue
NameDeploy Pi-hole and Unbound on Ubuntu (Docker)
Hostspihole_server
Becomeyes
Variablesproject_dir, pihole_password

Variables

project_dir
string
default:"/root/pihole-docker"
The directory on the target host where the Docker Compose project is created. The playbook creates this directory with permissions 0755 if it does not already exist.
pihole_password
string
default:"1234"
The Pi-hole web admin password set via the WEBPASSWORD environment variable in the Docker Compose configuration.
Change pihole_password from the default 1234 before running the playbook. The default value is insecure and will be embedded in the generated docker-compose.yml in plain text.

Tasks

All 10 tasks execute in sequence. The playbook is designed to be safe to run multiple times — idempotent modules (apt, file, copy, lineinfile, ufw) will not duplicate work on subsequent runs.
1

1. Stop and Disable Native Services

Stops and disables four services that would conflict with the Docker containers by occupying ports 53, 80, and 5335.
  • Module: service
  • State: stopped, enabled: no
  • Services: pihole-FTL, unbound, lighttpd, systemd-resolved
  • ignore_errors: yes — prevents the playbook from failing if any service is not installed
- name: 1. Stop and Disable native services
  service:
    name: "{{ item }}"
    state: stopped
    enabled: no
  loop:
    - pihole-FTL
    - unbound
    - lighttpd
    - systemd-resolved
  ignore_errors: yes
2

2. Fix DNS Resolution for Docker

Disables the systemd-resolved DNS stub listener so that Docker containers can use the host’s DNS without conflict. Notifies the Restart systemd-resolved handler to apply the change.
  • Module: lineinfile
  • File: /etc/systemd/resolved.conf
  • Regexp: ^#?DNSStubListener=
  • Line: DNSStubListener=no
  • Notifies: Restart systemd-resolved handler
- name: 2. Fix DNS resolution for Docker
  lineinfile:
    path: /etc/systemd/resolved.conf
    regexp: '^#?DNSStubListener='
    line: 'DNSStubListener=no'
  notify: Restart systemd-resolved
3

3. Create Generic resolv.conf

Overwrites /etc/resolv.conf with a minimal Google DNS entry so the host has working DNS resolution during the rest of the play.
  • Module: copy
  • Destination: /etc/resolv.conf
  • Content: nameserver 8.8.8.8
  • Force: yes
- name: 3. Create generic resolv.conf (Google DNS)
  copy:
    dest: /etc/resolv.conf
    content: "nameserver 8.8.8.8"
    force: yes
4

4. Remove Conflicting Docker Packages

Purges any conflicting Docker packages that may be present from a prior installation. This prevents version conflicts with the docker.io package installed in the next task.
  • Module: apt
  • State: absent
  • Purge: yes
  • Packages: docker-ce, docker-ce-cli, containerd.io, docker-compose-plugin
- name: 4. Remove conflicting Docker packages
  apt:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-compose-plugin
    state: absent
    purge: yes
5

5. Install Dependencies

Installs all required packages. update_cache: yes ensures the APT cache is refreshed before the install.
  • Module: apt
  • State: present
  • Packages: curl, net-tools, docker.io, docker-compose-v2
- name: 5. Install dependencies
  apt:
    name:
      - curl
      - net-tools
      - docker.io
      - docker-compose-v2
    state: present
    update_cache: yes
6

6. Create Project Directory

Creates the project directory defined by project_dir. The directory is created with mode 0755. This task is idempotent — it will not fail if the directory already exists.
  • Module: file
  • Path: {{ project_dir }}
  • State: directory
  • Mode: 0755
- name: 6. Create Project Directory
  file:
    path: "{{ project_dir }}"
    state: directory
    mode: '0755'
7

7. Create Docker Compose File

Writes the complete docker-compose.yml to {{ project_dir }}. The pihole_password variable is interpolated into the WEBPASSWORD environment variable. See Docker Compose File below for the full output.
  • Module: copy
  • Destination: {{ project_dir }}/docker-compose.yml
- name: 7. Create Docker Compose file
  copy:
    dest: "{{ project_dir }}/docker-compose.yml"
    content: |
      # ... (full compose content)
8

8. Launch Containers

Starts the Pi-hole and Unbound containers in detached mode using the Compose file created in task 7.
  • Module: command
  • Command: docker compose up -d
  • Working directory: {{ project_dir }}
- name: 8. Launch Containers
  command: docker compose up -d
  args:
    chdir: "{{ project_dir }}"
9

9. Configure UFW

Opens the three required inbound TCP ports using the ufw module. Loops over port numbers 22, 80, and 53.
  • Module: ufw
  • Rule: allow
  • Protocol: tcp
  • Ports: 22, 80, 53
- name: 9. Configure Firewall (UFW)
  ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - '22'
    - '80'
    - '53'
10

10. Enable UFW

Enables UFW with the rules applied in the previous task.
  • Module: ufw
  • State: enabled
- name: 10. Enable Firewall
  ufw:
    state: enabled

Handler

The playbook defines one handler, triggered by task 2:
Handler nameModuleAction
Restart systemd-resolvedserviceRestarts the systemd-resolved service to apply the DNSStubListener=no change
handlers:
  - name: Restart systemd-resolved
    service:
      name: systemd-resolved
      state: restarted

Docker Compose File

This is the complete docker-compose.yml that the playbook writes to {{ project_dir }}/docker-compose.yml. The WEBPASSWORD field is populated from the pihole_password variable at runtime.
services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: 'Europe/Madrid'
      WEBPASSWORD: '1234'
      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
Pi-hole forwards all DNS queries to Unbound at 172.20.0.5#5335 via the custom pihole_net Docker bridge network. The PIHOLE_DNS_2: 'no' setting disables any secondary upstream, making Unbound the sole resolver.

Inventory

The default inventory.ini targets localhost via a local Ansible connection:
[pihole_server]
localhost ansible_connection=local
To deploy to a remote host, replace localhost with the target IP address or hostname and set the appropriate SSH connection parameters:
[pihole_server]
192.168.1.50 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa
The pihole_server group name must match the hosts: field in install_pihole.yml.

Running the Playbook

1

Clone the repository

git clone https://github.com/damianiglesias/pihole-ubuntu-deploy
cd pihole-ubuntu-deploy/ansible
2

Install Ansible

sudo apt update && sudo apt install ansible -y
3

Edit inventory and variables

Update inventory.ini with your target host and override pihole_password with a secure value — either in the playbook vars block or via --extra-vars on the command line.
4

Run the playbook

ansible-playbook -i inventory.ini install_pihole.yml
To override the password at runtime without editing the file:
ansible-playbook -i inventory.ini install_pihole.yml \
  --extra-vars "pihole_password=MySecurePass"

Build docs developers (and LLMs) love