Skip to content

Pi-hole Maintenance and Advanced Configuration

Tested

After the Pi is set up and running, use this page as a reference for confirming sudo requires a password, running manual updates, configuring backups of your Pi configuration, using a USB drive to store logs, or setting how long query history is kept.

All of these sections are optional.

Want Claude Code to help diagnose issues using these logs? See Give Claude Code Access to Pi-hole Logs and Diagnostics.

Optional: Require a Password for sudo Commands

Section titled “Optional: Require a Password for sudo Commands”
Tested

The non-root user that Raspberry Pi Imager created might be able to run sudo commands without a password.

Raspberry Pi OS grants that through a file in /etc/sudoers.d/ named after the account, such as 010_pi-admin-nopasswd. Remove the file so that sudo commands require authentication.

Raspberry Pi OS 6.2 (April 2026) stopped creating that file on new installs, so if you flashed your SD card with a current copy of Raspberry Pi Imager, you almost certainly don’t have it. This matters more if you’re rebuilding on an older image or setting up an existing device, but the command below handles both cases either way.

  1. Remove the configuration file if it exists:

    From the Pi
    sudo sh -c 'rm -fv /etc/sudoers.d/*-nopasswd'

    The glob runs inside sh -c so that root expands it. /etc/sudoers.d/ is readable only by root, so your own shell can’t match the filename before sudo starts.

    A removed file is named in the output. No output means there was nothing to remove.

  2. Confirm no passwordless rule survives anywhere else in the directory:

    From the Pi
    sudo grep -rl NOPASSWD /etc/sudoers.d/

    This prints nothing when passwordless sudo is off. Any file it names still grants it, and you should read that file before removing it.

  3. Log out of the SSH session:

    Terminal window
    exit
  4. Log back in:

    Terminal window

Update the Pi-hole software and refresh the Gravity database with updated lists.

Run these whenever Pi-hole releases a new version, or after adding or changing blocklists.

From the Pi
sudo pihole -up && \
sudo pihole -g

Pi-hole’s Teleporter exports your full configuration: blocklists, allowlists, custom domains, regex rules, DNS settings, and DHCP config.

  1. In the Pi-hole web interface, go to Settings > Teleporter.
  2. Select Export to download a backup file.

To restore after a rebuild or SD card failure, use the Import option on the same page and select the backup file.

Re-export after making significant changes to lists or settings. Store the backup file somewhere other than the Pi, such as on your local computer, a NAS, or cloud storage.

Treat the file as sensitive. It includes your full configuration, so store it like any config backup that might hold credentials: never in a shared or public folder.

Advanced Pi-hole Configuration with pihole.toml

Section titled “Advanced Pi-hole Configuration with pihole.toml”

If you need to configure Pi-hole’s DNS behavior outside the web interface, edit /etc/pihole/pihole.toml directly.

After editing, restart the FTL service to apply changes:

From the Pi
sudo systemctl restart pihole-FTL

pihole-FTL is the core Pi-hole system service. It needs to be restarted when you make core configuration changes.

Change the addresses in the [dns] section of pihole.toml:

pihole.toml
...
[dns]
upstreams = ["127.0.0.1#5335", "1.1.1.1"] # unbound on localhost, Cloudflare backup
...

Or for Cloudflare directly:

pihole.toml
...
[dns]
upstreams = ["1.1.1.1", "1.0.0.1"]
...

This is the full pihole.toml from my setup, with comments stripped and the password hash redacted. Values marked ### CHANGED were modified from the Pi-hole default.

pihole.toml (v6.7, comments stripped)
# Pi-hole configuration file (v6.7)
# Reference config - comments stripped, default values preserved for reference.
# Values marked ### CHANGED were modified from the Pi-hole default.
# To apply: copy to /etc/pihole/pihole.toml, then sudo systemctl restart pihole-FTL
[dns]
upstreams = [
"127.0.0.1#5335",
"1.1.1.1"
] ### CHANGED, default = []
CNAMEdeepInspect = true
blockESNI = true
EDNS0ECS = false ### CHANGED, default = true
ignoreLocalhost = false
showDNSSEC = true
analyzeOnlyAandAAAA = false
piholePTR = "PI.HOLE"
replyWhenBusy = "ALLOW"
blockTTL = 2
hosts = []
domainNeeded = false
expandHosts = false
bogusPriv = true
dnssec = false
interface = "eth0" ### CHANGED, default = ""
hostRecord = ""
listeningMode = "ALL" ### CHANGED, default = "LOCAL"
queryLogging = true
cnameRecords = []
port = 53
localise = true
revServers = []
[dns.domain]
name = "lan"
local = true
[dns.cache]
size = 10000
optimizer = 3600
upstreamBlockedTTL = 86400
rrtype = "ANY"
[dns.blocking]
active = true
mode = "NULL"
edns = "TEXT"
[dns.specialDomains]
mozillaCanary = true
iCloudPrivateRelay = true
designatedResolver = true
[dns.reply.host]
force4 = false
IPv4 = ""
force6 = false
IPv6 = ""
[dns.reply.blocking]
force4 = false
IPv4 = ""
force6 = false
IPv6 = ""
[dns.rateLimit]
count = 1000
interval = 60
[dhcp]
active = false
start = ""
end = ""
router = ""
netmask = ""
leaseTime = ""
ipv6 = false
rapidCommit = false
multiDNS = false
logging = false
ignoreUnknownClients = false
hosts = []
[ntp]
[ntp.ipv4]
active = true
address = ""
[ntp.ipv6]
active = true
address = ""
[ntp.sync]
active = false ### CHANGED, default = true
server = "pool.ntp.org"
interval = 3600
count = 8
[ntp.sync.rtc]
set = false
device = ""
utc = true
[resolver]
resolveIPv4 = true
resolveIPv6 = true
macNames = true
networkNames = true
refreshNames = "IPV4_ONLY"
[database]
DBimport = true
maxDBdays = 91
DBinterval = 60
useWAL = true
forceDisk = false
[database.network]
parseARPcache = true
expire = 91
[webserver]
domain = "pi.hole"
acl = ""
port = "80o,443os,[::]:80o,[::]:443os"
threads = 50
headers = [
"X-DNS-Prefetch-Control: off",
"Content-Security-Policy: default-src 'none'; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; img-src 'self'; manifest-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'self'",
"X-Frame-Options: DENY",
"X-XSS-Protection: 0",
"X-Content-Type-Options: nosniff",
"Referrer-Policy: strict-origin-when-cross-origin"
]
serve_all = false
advancedOpts = []
[webserver.session]
timeout = 1800
restore = true
[webserver.tls]
cert = "/etc/pihole/tls.pem"
validity = 47
[webserver.paths]
webroot = "/var/www/html"
webhome = "/admin/"
prefix = ""
[webserver.interface]
boxed = true
theme = "default-auto"
[webserver.api]
max_sessions = 16
prettyJSON = false
pwhash = "<redacted>" ### CHANGED from default (use `sudo pihole setpassword` to set)
totp_secret = ""
app_pwhash = ""
app_sudo = false
cli_pw = true
excludeClients = []
excludeDomains = []
maxHistory = 86400
maxClients = 10
client_history_global_max = true
allow_destructive = true
[webserver.api.temp]
limit = 60.000000
unit = "C"
[files]
database = "/etc/pihole/pihole-FTL.db"
tmp_db = "/etc/pihole/pihole-tmp.db"
gravity = "/etc/pihole/gravity.db"
gravity_tmp = "/tmp"
macvendor = "/etc/pihole/macvendor.db"
pcap = ""
[files.log]
ftl = "/var/log/pihole/FTL.log"
dnsmasq = "/var/log/pihole/pihole.log"
webserver = "/var/log/pihole/webserver.log"
[misc]
privacylevel = 0
delay_startup = 0
nice = -10
addr2line = true
etc_dnsmasq_d = false
dnsmasq_lines = []
extraLogging = false
readOnly = false
normalizeCPU = true
hide_dnsmasq_warn = false
hide_connection_error = false
[misc.check]
load = true
shmem = 90
disk = 90
[debug]
database = false
networking = false
locks = false
queries = false
flags = false
shmem = false
gc = false
arp = false
regex = false
api = false
tls = false
overtime = false
status = false
caps = false
dnssec = false
vectors = false
resolver = false
edns0 = false
clients = false
aliasclients = false
events = false
helper = false
config = false
inotify = false
webserver = false
extra = false
reserved = false
ntp = false
netlink = false
timing = false
performance = false
all = false
Community-reported

The Pi-hole FTL database (pihole-FTL.db) grows continuously and can wear out an SD card over months. Move the database and log file to a USB drive to protect the SD card and extend its life.

  • A USB flash drive
  • The Pi running with Pi-hole installed
  1. Plug in the USB drive and find its device name:

    From the Pi
    lsblk

    Look for a /dev/ that wasn’t there before, such as /dev/sda with a partition at /dev/sda1.

  2. Get the partition’s UUID.

    This example is intentionally set to fail if you don’t adjust YOUR-USB-PARTITION to the device you found in the previous step:

    From the Pi
    sudo blkid /dev/YOUR-USB-PARTITION

    Copy the UUID value from the output.

  3. Create a mount point:

    From the Pi
    sudo mkdir -p /mnt/pihole-logs
  4. Add an entry to /etc/fstab to auto-mount on boot. Open the file and add this line at the bottom, replacing YOUR-UUID with the UUID you copied:

    /etc/fstab
    UUID=YOUR-UUID /mnt/pihole-logs ext4 defaults,noatime 0 2

    Note that this example uses ext4. If your USB drive is formatted as exFAT or NTFS instead of ext4, change the filesystem type accordingly.

    If you need to format the drive first

    This deletes everything on the drive.

    1. Confirm which device is the USB drive, not an internal disk:

      From the Pi
      lsblk -o NAME,SIZE,LABEL,MOUNTPOINT

      Match the size and label to your USB drive. Don’t guess: an internal disk can share the same /dev/sdX naming scheme, and mkfs won’t ask for confirmation before it formats.

      It deletes everything on the drive.

    2. Format the partition.

      This example is intentionally set to fail if you don’t adjust YOUR-USB-PARTITION:

      From the Pi
      sudo mkfs.ext4 /dev/YOUR-USB-PARTITION
  5. Mount everything in fstab:

    From the Pi
    sudo mount -a

    If this returns an error, double-check the UUID and filesystem type in /etc/fstab.

  6. Set ownership so Pi-hole’s FTL process can write to the drive:

    From the Pi
    sudo chown pihole:pihole /mnt/pihole-logs
  1. Back up the current pihole.toml:

    From the Pi
    sudo cp /etc/pihole/pihole.toml /etc/pihole/pihole.toml.bak
  2. Move the existing database to the USB drive (so you don’t lose query history):

    From the Pi
    sudo systemctl stop pihole-FTL && \
    sudo mv /etc/pihole/pihole-FTL.db /mnt/pihole-logs/ && \
    sudo mv /var/log/pihole/pihole.log /mnt/pihole-logs/ 2>/dev/null || true
  3. Edit /etc/pihole/pihole.toml to update the file paths. Find the [files] section and set:

    pihole.toml
    ...
    [files]
    database = "/mnt/pihole-logs/pihole-FTL.db"
    [files.log]
    dnsmasq = "/mnt/pihole-logs/pihole.log"
    ...

    log is a subsection ([files.log]), not a single value. Only dnsmasq needs to change here; ftl and webserver can stay on the SD card. If there is no [files] section, add it at the end of the file.

  4. Start FTL and verify it comes up cleanly:

    From the Pi
    sudo systemctl start pihole-FTL && \
    sudo systemctl status pihole-FTL
  5. Confirm the database is on the USB drive and growing:

    From the Pi
    ls -lh /mnt/pihole-logs/
  6. Reboot the Pi:

    From the Pi
    sudo reboot
  7. Verify that the USB drive mounts and Pi-hole starts correctly:

    From the Pi
    lsblk && \
    systemctl status pihole-FTL

    The FTL service won’t start if it can’t find the database file. DNS queries from all network devices will fail until the mount is fixed. See FTL database grows too large and FTL fails to start if FTL won’t come back up.

Tested

By default, Pi-hole keeps 91 days of query history.

If you want to change how many days of history Pi-hole keeps:

  1. Set the number of days to keep the database in pihole.toml:

    pihole.toml
    ...
    [database]
    maxDBdays = 180
    ...
  2. Restart FTL after changing this:

    From the Pi
    sudo systemctl restart pihole-FTL