Updated and tested 17 October 2026
You opened /etc/pihole/pihole.toml in nano, changed upstreams, saved, restarted pihole-FTL for good measure, and the line is back the way it was. The header now says # Last updated on a minute ago. Discourse thread 78859, "Pihole.toml file keeps reverting", is the same story with a different key. Nothing is broken. FTL owns that file, it noticed your edit, decided the value was not acceptable and wrote its own copy over yours.
The rest of this page is the contract: what the file is, the five ways to change a value, what FTL does the moment the file changes on disk and which keys restart the daemon.
What the pihole.toml header tells you
One file, /etc/pihole/pihole.toml, replaced setupVars.conf, pihole-FTL.conf and the 01-pihole.conf family in v6. FTL writes it, so it always contains every setting, each with its documentation comment, and the first lines read:
# Pi-hole configuration file (v6.8)
# Encoding: UTF-8
# This file is managed by pihole-FTL
# Last updated on ...
A value that differs from the default gets a trailing annotation, ### CHANGED, default = ..., or ### CHANGED (env), default = ... when an environment variable set it. The file ends with a # Configuration statistics: block counting how many entries are at their default. The FTL configuration reference is generated from the same source as the file, so the defaults it shows are the ones FTL writes. Those annotations are the fastest way to audit a Pi-hole you did not set up: grep CHANGED /etc/pihole/pihole.toml lists everything that was ever touched. An array spans several lines, so its annotation lands on the closing bracket and the grep prints ] ### CHANGED, default = [] with no key name; grep -B3 CHANGED shows the lines above it.
grep CHANGED lists every key that differs from its default; arrays show only their closing bracket.
Five ways to set a value
The reference page lists them and then says which to prefer: "Using the web interface, the API or the CLI is preferred as they can do error checking for you".
- The CLI:
sudo pihole-FTL --config dns.upstreamsprints the value,sudo pihole-FTL --config dns.upstreams '[ "127.0.0.1#5335" ]'sets it andsudo pihole-FTL --config -t dns.listeningMode SINGLEtests a value without applying it. - The web interface: Settings, with the Basic/Expert toggle at the top right to expose every key. On a VPS behind WireGuard that is
http://10.100.0.1/admin/. - The API:
/api/config, with the session ID from/api/auth. - Environment variables:
FTLCONF_<section>_<key>, sodns.upstreamsbecomesFTLCONF_dns_upstreams. Arrays are semicolon-separated. - The file itself, with the behaviour described in the next section.
The Docker configuration page writes the CLI form as --config setting.name=value; the FTL help text and the reference use a space between key and value, and the space form is the one the examples on this page use.
What FTL does when the file changes on disk
None of this is in the docs prose; it comes from reading the FTL source, src/config/inotify.c and the reread_config() function in src/config/config.c. FTL keeps an inotify watch on /etc/pihole and reacts when pihole.toml is written and closed, or moved into place. On a change it checksums the file, and if the checksum differs from the last one it logs Reloading config due to pihole.toml change to /var/log/pihole/FTL.log and parses the file against the validators.
A valid edit is installed without a restart, unless one of the changed keys is flagged to need one, in which case FTL logs Restarting FTL due to change of <key> and restarts itself. A value that fails its validator isn't kept: FTL logs Invalid value with the reason and ----> <key> has been reset to its default value, installs the rest of the file with that key at its default, and writes the file back to disk. A key with a fixed list of options behaves differently: listeningMode = "SINGEL" logs Config setting dns.listeningMode is invalid and the key keeps the value it had before the edit. Since FTL v6.8 an array such as dns.upstreams loses only the entries that fail, each logged as ----> <key>[n] has been removed, and keeps the rest. A file that won't parse at all is dropped whole: Cannot parse TOML file in the log, the running configuration stays as it was, and the file is written back from it. Either way every option and every annotation is present again afterwards. That rewrite is why the header timestamp moved and why the typo is gone.
So a hand edit is fine when the value is right, and the way to know is the log:
sudo tail -f /var/log/pihole/FTL.log
Edit, save, watch. One Reloading config line and nothing red means it took. I keep that tail open in a second terminal for as long as the file is open in the first; it has caught more of my typos than reading the file back ever did. Pushing the file from Ansible? The same check applies to configuration management: a template that writes the whole file works, provided every value in it passes the validators, and the -t test above is how to find out before the deploy.
At startup the logic differs slightly. FTL reads the file and rewrites it; if it cannot parse the file at all it walks the rotated copies in /etc/pihole/config_backups from newest to oldest until one parses, logs No config file nor backup available, using defaults if none does and renames the unreadable file to pihole.toml.broken before writing defaults. A stopped FTL and a careful edit is therefore a supported path; a TOML syntax error is the one mistake that costs you the file.
Restart-flagged keys in pihole.toml
Restart-flagged keys in config.c include dns.upstreams, dns.interface, dns.listeningMode, dns.port, dns.dnssec, dns.queryLogging, webserver.port, webserver.domain, webserver.acl, webserver.tls.cert, misc.etc_dnsmasq_d and the dhcp.* and ntp.* activation switches. Setting any of them through the CLI, the API or the web interface restarts the daemon on its own, so a pihole-FTL --config dns.listeningMode SINGLE on a live resolver costs a few seconds of DNS and nothing else needs to be typed.
Changing a restart-flagged key makes FTL restart itself.
A changed webserver.api.pwhash, by any route, invalidates every open web session, which is what the reset the Pi-hole v6 admin password page relies on. pihole setpassword writes the pseudo-key webserver.api.password and FTL stores the hash; the plaintext never lands in the file.
Rotated copies in config_backups
Every time FTL replaces pihole.toml, including after an import with Teleporter on Pi-hole v6, it rotates the previous version into /etc/pihole/config_backups/ first. That directory is a short undo history. It lives on the same disk and a reinstalled VPS takes it along, so the zip Teleporter exports is the copy to keep off the server.
Read-only keys from environment variables
A key set through FTLCONF_* becomes read-only. The Docker configuration page says it in one sentence: "All FTL settings that are set via environment variables effectively become read-only, meaning that you will not be able to change them via the web interface or CLI." The file marks such a key ### CHANGED (env), and the same page adds that removing the variable later makes FTL revert that setting to its default.
The whole file can be frozen the same way with misc.readOnly = true, which the reference describes as the setting for a configuration "to be forced/modified by some third-party application (like infrastructure-as-code providers)". It blocks changes through the API and CLI; the file and environment variables still apply. Set it in the file itself, as readOnly = true under [misc], because the CLI refuses that one key with Config option misc.readOnly can only be set in pihole.toml, not via the CLI. Once it's on, a CLI change prints Config is in read-only mode, changes are not allowed (misc.readOnly = true), while a hand edit of the file still reloads.
The dnsmasq side of the configuration
FTL turns the [dns] section into /etc/pihole/dnsmasq.conf on every start. The header of that generated file says any changes made to it will be lost when the configuration changes and points back at pihole.toml. Extra dnsmasq directives go in one of two keys: misc.etc_dnsmasq_d = true makes FTL read /etc/dnsmasq.d/*.conf again, and misc.dnsmasq_lines takes an array of single lines. pihole reloaddns does not re-read either; a config change there needs a restart of pihole-FTL.
Since FTL v6.7.1, a security release of 19 September 2026, misc.dnsmasq_lines and webserver.advancedOpts stopped accepting changes from the API and the web interface (GHSA-ww5x-xx4x-qvjr and GHSA-2794-hrj8-5jg9). The reference explains why: dnsmasq directives such as dhcp-script and conf-script name programs dnsmasq then executes, which is the remote code execution the advisory describes. Both keys stay settable in the file, by environment variable and with pihole-FTL --config, which all require a shell on the host.



