Pi-Hole on TrueNAS
Introduction
This blog entry outlines the steps to setup Pi-Hole together with Bind9 on TrueNAS Scale, making use of its default container runtime (LXC).
Prerequisites
- Up and running TrueNAS Scale
Pi-Hole Setup
Login to TrueNAS web console, go to Datasets and create a new dataset named pi-hole for the Pi-Hole app - assuming one dataset per app. Depending on the setup, you may need to SSH into the TrueNAS host and set permissions accordingly.
A file and folder structure may look like this:
1
2
3
/mnt/apps/pi-hole
/mnt/apps/pi-hole/config
/mnt/apps/pi-hole/compose.yaml
The content of the compose.yaml file may look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
services:
pi-hole:
image: pihole/pihole:2025.11.1
ports:
# DNS
- "192.168.1.170:53:53/tcp"
- "192.168.1.170:53:53/udp"
networks:
- nginx
- dns
environment:
PIHOLE_UID: ${PUID:-950}
PIHOLE_GID: ${PGID:-950}
TZ: 'Europe/Zurich'
# If using Docker's default `bridge` network setting the dns listening mode should be set to 'all'
FTLCONF_dns_listeningMode: all
# Sets upstream DNS to internal bind9 dns server
FTLCONF_dns_upstreams: bind9
volumes:
- './config/pi-hole:/etc/pihole'
restart: always
cap_add:
# See https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
# Optional, if Pi-hole should get some more processing time
- SYS_NICE
networks:
nginx:
external: true
dns:
external: true
Explanations to be well noted:
- The user/group to start the container process and have at least read permission to the dataset, can be set through the PUID and PGID env var, or use default values.
- Access to the Pi-Hole web interface is provided through the
nginxnetwork, respectively via Nginx reverse proxy. - DNS forwarding to an internal Bind9 DNS server goes through the
dnsnetwork. - Using the
FTLCONF_dns_upstreamsenv var, the internal Bind9 DNS server is configured as upstream DNS server for Pi-Hole. - The networks are assumed external, to be created by the concerned apps.
- Because of more than one IP address configured with the network interface, the ports binding also includes an explicit IP address to bind to.
To deploy and run Pi-Hole like a TrueNAS app, go to the Apps section in the TrueNAS web console, click Discover Apps, then click on the three dots on the upper right and click Install via YAML.
Insert the name of the app (e.g. pi-hole) and copy/paste the following custom configuration (adjust path if needed):
1
2
include:
- /mnt/apps/pi-hole/compose.yaml
TrueNAS should do the deployment and indicate once the app is running.
Bind9 Setup
The steps to deploy and run Bind9 as an internal DNS server, are very similar to the ones for Pi-Hole.
After creating a new dataset named bind9 and adjusting permissions, a few config files need to to be created:
1
2
3
4
5
6
7
8
/mnt/apps/bind9
/mnt/apps/bind9/config
/mnt/apps/bind9/config/my.domain.io.zone
/mnt/apps/bind9/config/named.conf
/mnt/apps/bind9/config/named.conf.options
/mnt/apps/bind9/config/named.conf.local
/mnt/apps/bind9/config/named.conf.logging
/mnt/apps/bind9/compose.yaml
named.conf:
1
2
3
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.logging";
named.conf.options:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// only internal addresses
acl internal {
192.168.1.0/24; // internal subnet
172.16.0.0/16; // Docker subnet
localhost;
localnets;
};
options {
directory "/var/cache/bind";
forwarders {
1.1.1.1;
1.0.0.1;
};
allow-query { internal; };
allow-recursion { internal; };
allow-query-cache { internal; };
dnssec-validation no;
};
named.conf.local:
1
2
3
4
zone "my.domain.io" IN {
type master;
file "/etc/bind/my.domain.io.zone";
};
named.conf.logging:
1
2
3
4
5
6
7
8
9
10
11
12
13
logging {
channel stdout_channel {
stderr;
severity info;
print-category yes;
print-severity yes;
print-time yes;
};
category default { stdout_channel; };
category queries { stdout_channel; };
category security { stdout_channel; };
category dnssec { stdout_channel; };
};
compose.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
bind9:
image: ubuntu/bind9:latest
environment:
- TZ=Europe/Zurich
ports:
- "192.168.1.170:5453:53/tcp"
- "192.168.1.170:5453:53/udp"
volumes:
- ./config/bind9/named.conf:/etc/bind/named.conf
- ./config/bind9/my.domain.io.zone:/etc/bind/my.domain.io.zone
- ./config/bind9/named.conf.options:/etc/bind/named.conf.options
- ./config/bind9/named.conf.logging:/etc/bind/named.conf.logging
- ./config/bind9/named.conf.local:/etc/bind/named.conf.local
- bind9_cache:/var/cache/bind
- bind9_records:/var/lib/bind
restart: always
volumes:
bind9_cache:
bind9_records:
networks:
default:
name: dns
Insert the name of the app (e.g. pi-hole) and copy/paste the following custom configuration (adjust path if needed):
1
2
include:
- /mnt/apps/pi-hole/compose.yaml
Like before, go to the Apps section in the TrueNAS web console, click Discover Apps, then click on the three dots on the upper right and click Install via YAML.
Insert the name of the app (e.g. bind9) and copy/paste the following custom configuration (adjust path if needed):
1
2
include:
- /mnt/apps/bind9/compose.yaml
TrueNAS should do the deployment and indicate once the app is running.
Verification
To verify that the setup is working, use dig command to query at different levels.
1
2
3
4
5
# Query Bind9 directly
dig @192.168.1.170 -p 5353 my.domain.io
# Query Pi-Hole
dig @192.168.1.170 -p 53 my.domain.io
Once verified, configure the local DHCP server to use the Pi-Hole IP address as DNS server. Then, renew the IP address lease and test from any client device if DNS resolution works.
Conclusion
With this setup, Pi-Hole acts as a DNS sinkhole, blocking black-listed domains and forwarding valid ones to the internal Bind9 DNS server acting as upstream DNS server.
Bind9 provides several additional features, like local zones, recursive DNS, or forwarding to public DNS servers.