Unattended Pi-hole v6 Setup with Ansible

Monday, 17th March 2025

There's an undocumented feature on the Pi-hole installer which allows for an unattented (no user interaction, setup questions, etc) install. For previous versions of Pi-hole This involved providing a file which would contain the information needed for the answers to the questions normally asked as part of the setup process.

However, with the release of Pi-hole v6, almost all of the Pi-hole's configuration is now saved in a single .toml file. If the installer detects this file is in place then it will not treat the setup as a "fresh install", and instead preserve the settings in the file and skip the normal setup questions - more like an upgrade than a clean install.

This is great for automating the installation on my Raspberry Pi's, especially since I decided to run an additional Pi-hole as a secondary DNS for my network. Not only can I automate the setup with Ansible, but it means that the configuration can be done once and deployed to both of them at the same time, keeping everything in sync.

Step-by-step

To begin with, I used Raspberry Pi Imager to get a clean install of the latest 64 bit "Lite" version of Raspberry Pi OS onto my two Pi 4's. This is an easy way to setup a default user/password, a hostname and to enable the SSH server while the image is being written.

I then add a new role to my main ansible playbook - provision.yml:-

---
- name: Common setup (same for all hosts)
  hosts: all
  become: true
  roles:
    - { role: common }

- name: Pi-holes
  hosts:
    - pihole1.home.arpa
    - pihole2.home.arpa
  become: true
  roles:
    - { role: pihole6 }

I won't dig into the "common" role today, but it installs a few packages I use on all my servers, like git, unzip, nano, etc.

For the new pihole6 role, I create a tasks yaml file at roles/pihole6/tasks/main.yml where I will add all the modules for setting things up.

First off, I need a user and group. These are matched to what the pihole installer will create if they didn't already exist:-

---
- name: Create pihole group
  ansible.builtin.group:
    name: pihole
    state: present

- name: Create pihole user
  ansible.builtin.user:
    name: pihole
    home: "/home/pihole"
    shell: "/usr/sbin/nologin"
    comment: "PiHole"
    group: pihole
    password: "*"
    state: present
    remove: true
    force: true

Next, I needed to generate a pihole.toml file which includes all my customisations.

The complete 'pihole.toml' file is over 1,000 lines long and is very well documented with inline comments - so I'm not going to include the whole thing here. You can view a test version on GitHub, but to generate the one I needed, I actually manually installed Pi-hole following the docs, and configured it as I wanted then extracted the /etc/pihole/pihole.toml from the finished setup then removed it again with pihole --uninstall.

Next, I add the items to my role to create the pihole config directory under /etc/ and copy my customised pihole.toml file into place:-

- name: Create config directory
  ansible.builtin.file:
    state: directory
    path: /etc/pihole
    owner: pihole
    group: pihole
    mode: "775"

- name: Pre-configure pihole
  ansible.builtin.copy:
    src: pihole.toml
    dest: /etc/pihole/pihole.toml
    owner: pihole
    group: pihole
    mode: "644"

With these few tasks completed, I can now add a task to run the installer in unattended mode:-

- name: Install Pi-hole
  ansible.builtin.shell:
    cmd: curl -sSL https://install.pi-hole.net | bash /dev/stdin --unattended
    creates: /etc/systemd/system/pihole-FTL.service

The "creates" line will prevent the installer running every time I run the ansible playbook.

Because the pihole.toml file was already in-place, the installer runs more like an upgrade than a "fresh install", so the final task is to update the gravity lists to activate them:-

- name: Update gravity lists
  ansible.builtin.shell:
    cmd: pihole -g

I don't need a "creates" line here, as there is no harm in running this command multiple times.

With my playbook updated and role complete, I can provision just the two Raspberry Pi's which will become Pi-hole's:-

$ ansible-playbook -l pihole1,pihole2, privision.yml

The idea behind creating ansible playbooks like this is that they are idempotent - you define what you want the system to be like and you can run the playbook multiple times, and ansible will only update the things which DON'T already match your desired config.

However, I'm bending that ideal scenario for one final task:-

- name: Update Pi-hole
  ansible.builtin.shell:
    cmd: pihole -up

So in the future, I will just run ansible-playbook provision.yml and the Pi-hole's will be updated along with the other servers I run on my network - web, mail, grafana and even minecraft servers (topics for future posts). All updated (or reinstalled from a default Raspberry Pi OS) with a single command.