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.