Pi-hole configuration as code.
ConfigHole is a small tool for managing one or more Pi-hole instances from a single YAML file. It is aimed at homelabs and self-hosted setups where you want your DNS configuration to be repeatable, reviewable, and easy to keep in sync.
It is built on top of another project of mine, pihole-lib, a Python library that talks to the Pi-hole API.
You can use this tool as a one-off to bootstrap an initial configuration, or run it as a daemon that periodically checks for changes in your local config.
It’s one of those tools where the time spent building it far exceeds the time you’ll ever spend manually tweaking settings - see https://xkcd.com/1319.
- Disclaimer
- Supported Pi-hole Versions
- Features
- Installation
- Usage
- Daemon Mode
- Configuration
- Development
- Contributing
- Author
- License
This project is not affiliated with, endorsed by, or supported by Pi-hole LLC. Pi-hole is a trademark of Pi-hole LLC.
ConfigHole was written for my own homelab to keep Pi-hole configuration under version control and in sync (I use Flux to manage my Kubernetes clusters). It is a personal tool that escaped into a public repository, and it is built for homelabs rather than production environments.
It works for me, but it may not work for you. There are no warranties, no guarantees, and no promises that it will not break your DNS at an inconvenient moment.
Read the code, test your changes, and use dry-run mode before trusting it with anything important.
Designed and tested against Pi-hole v6.0 and newer.
- Manage Pi-hole configuration declaratively using YAML
- Keep multiple Pi-hole instances in sync
- Manage blocklists and allowlists
- Manage domains (exact and regex, allow and deny)
- Manage groups
- Manage clients
- Optional automatic gravity update when lists change
- See exactly what will change before applying it
- Dry-run mode so you can test without touching anything
- Optional daemon mode for periodic reconciliation
Note
Example docker-compose file can be found at examples/docker-compose.yaml:
# One-time sync
$ docker run --rm \
-v $(pwd)/config:/config:ro \
-e PIHOLE_PASSWORD="$PIHOLE_PASSWORD" \
ghcr.io/dsgnr/confighole:latest -c config/config.yaml --sync
# Run daemon
$ docker run -d --name confighole-daemon \
--restart unless-stopped \
-v $(pwd)/config:/config:ro \
-e CONFIGHOLE_DAEMON_MODE=true \
-e CONFIGHOLE_CONFIG_PATH=/config/config.yaml \
-e CONFIGHOLE_DAEMON_INTERVAL=300 \
-e PIHOLE_PASSWORD="$PIHOLE_PASSWORD" \
ghcr.io/dsgnr/confighole:latestNote
Uses Python 3.13. The Python environment uses Poetry for package management. This must be installed.
$ git clone https://github.com/dsgnr/confighole.git
$ cd confighole
$ poetry install
$ poetry run confighole --helpCreate a config.yaml - An example can be seen at examples/config.yaml.
# Global settings applied to all instances
global:
timeout: 30
verify_ssl: false
verbosity: 1 # Log verbosity: 0=WARNING, 1=INFO, 2=DEBUG
dry_run: false # Enable dry-run
# Daemon mode settings
daemon_mode: false # Enable daemon mode by default
daemon_interval: 300 # Sync interval in seconds (5 minutes)
instances:
- name: home
base_url: http://192.168.1.100
password: "${PIHOLE_PASSWORD}"
update_gravity: true
config:
dns:
upstreams: ["1.1.1.1", "1.0.0.1"]
queryLogging: true
dnssec: true
hosts:
- ip: 192.168.1.1
host: router.lan
- ip: 192.168.1.10
host: nas.lan
cnameRecords:
- name: plex.lan
target: nas.lanTip
YAML anchors work with the config, so you can define a list of hosts/cnames once, and reference them for all instances. For example:
hosts: &hosts
- ip: 192.168.1.1
host: gateway.lab
- ip: 192.168.1.10
host: nas.lab
lists: &lists
- address: https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
type: deny
comment: StevenBlack's Unified Hosts List
groups: [0]
enabled: true
domains: &domains
- domain: ads.example.com
type: deny
kind: exact
comment: Block ads domain
groups: [0]
enabled: true
- domain: ".*\\.tracking\\..*"
type: deny
kind: regex
comment: Block tracking subdomains
groups: [0]
enabled: true
instances:
- name: home
base_url: http://192.168.1.100
password: "${PIHOLE_PASSWORD}"
update_gravity: false
config:
dns:
hosts: *hosts
lists: *lists
domains: *domainsSet your password:
$ export PIHOLE_PASSWORD="your-admin-password"or use the --dump argument to grab an existing config. You'll need to at least have the instance defined with name, base_url and password in your local config in order to connect:
$ docker run --rm \
-v $(pwd)/config:/config:ro \
ghcr.io/dsgnr/confighole:latest -c /config/config.yaml -i homelab --dump
- name: homelab
base_url: http://10.50.1.10
config:
dns:
upstreams:
- 127.0.0.1#5053
CNAMEdeepInspect: true
blockESNI: true
EDNS0ECS: true
ignoreLocalhost: false
hosts:
- ip: 10.50.1.1
host: rtr0
[...]Run commands:
# Dump current Pi-hole state
$ confighole -c config.yaml --dump
# Show what would change
$ confighole -c config.yaml --diff
# Sync with dry-run
$ confighole -c config.yaml --sync --dry-run
# Apply changes
$ confighole -c config.yaml --sync
# Run continuously (every 5 minutes by default)
$ confighole -c config.yaml --daemonDaemon mode is useful if you want your Pi-hole instances to drift as little as possible. It periodically compares the live state with your config and applies any differences.
# Default interval (5 minutes)
$ confighole -c config.yaml --daemon
# Custom interval
$ confighole -c config.yaml --daemon --interval 600
# Dry-run daemon (monitor only)
$ confighole -c config.yaml --daemon --dry-run
# Target a single instance
$ confighole -c config.yaml --daemon --instance home --interval 180Configuration Precedence:
- CLI arguments take highest precedence
- Global config settings are used if CLI arguments are not specified
- Default values are used as fallback
Example: If your config sets daemon_interval: 600 but you run with --interval 300, the CLI value wins.
Configure daemon mode using environment variables (useful for Docker):
| Variable | Description | Default |
|---|---|---|
CONFIGHOLE_DAEMON_MODE |
Enable daemon mode | false |
CONFIGHOLE_CONFIG_PATH |
Path to config file | Required |
CONFIGHOLE_DAEMON_INTERVAL |
Sync interval in seconds | 300 |
CONFIGHOLE_INSTANCE |
Target instance | All |
CONFIGHOLE_DRY_RUN |
Enable dry-run mode | false |
CONFIGHOLE_VERBOSE |
Log verbosity (0-2) | 1 |
Apply to all instances unless overridden:
timeout- Tor request timeout in secondsverify_ssl- To enable or disable TLS verificationpassword/password_env- For default authentication
Daemon mode settings:
daemon_mode- Enable daemon mode by default (true/false)daemon_interval- Sync interval in seconds (default:300)verbosity- Log verbosity level (0=WARNING,1=INFO,2=DEBUG)dry_run- Enable dry-run mode by default (true/false)
Per-instance configuration:
name- Instance identifierbase_url- Pi-hole web interface URLpassword- Admin password (supports${ENV_VAR})update_gravity- Automatically update gravity when lists change (true/false, default:false)config- Pi-hole configuration to managelists- Pi-hole lists to managedomains- Pi-hole domains to manage (exact/regex, allow/deny)groups- Pi-hole groups to manageclients- Pi-hole clients to manage
The subscribed allowlist or blocklists:
lists:
- address: https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
type: deny
comment: StevenBlack's Unified Hosts List
groups: [0]
enabled: trueDomains support both exact matches and regex patterns, for both allow and deny lists:
domains:
# Exact domain to block
- domain: ads.example.com
type: deny # deny or allow
kind: exact # exact or regex
comment: "Block ads"
groups: [0]
enabled: true
# Regex pattern to allow
- domain: ".*\\.trusted\\.com"
type: allow
kind: regex
comment: "Allow trusted subdomains"
groups: [0]
enabled: trueGroups allow you to organise clients and apply different filtering rules:
groups:
- name: family
comment: "Family devices"
enabled: true
- name: iot
comment: "IoT devices with strict filtering"
enabled: trueClients can be identified by IP address, MAC address, hostname, subnet (CIDR), or interface:
clients:
- client: "192.168.1.50"
comment: "John's laptop"
groups: [0, 1]
- client: "12:34:56:78:9A:BC"
comment: "Smart TV"
groups: [0]
- client: "192.168.2.0/24"
comment: "Guest network"
groups: [2]# Setup
$ poetry install
# Code quality
$ make lint # ruff linting
$ make format # ruff formatting
$ make type-check # mypy type checking
# All checks
$ make checkConfigHole includes a comprehensive test suite with both unit and integration tests:
# Run all tests
$ make test
# Run only unit tests (fast, no Pi-hole required)
$ make test-unit
# Run only integration tests (requires Pi-hole container)
$ make test-integration
# Run tests with coverage report
$ make test-coverage
# Start Pi-hole test container
$ make test-docker
# Stop Pi-hole test container
$ make test-docker-down
# Stop and clean up Pi-hole test container (removes volumes)
$ make test-docker-cleanIntegration tests spin up a real Pi-hole container from pi-hole/docker-pi-hole to verify functionality. The test suite automatically manages the container lifecycle and waits for Pi-hole to be ready before running tests.
The test suite automatically cleans up the Pi-hole container after tests complete. If tests are interrupted, you can manually clean up with make test-docker-clean.
I'm thrilled that you’re interested in contributing to this project! Here’s how you can get involved:
-
Submit Issues:
- If you encounter any bugs or have suggestions for improvements, please submit an issue on our GitHub Issues page.
- Provide as much detail as possible, including steps to reproduce and screenshots if applicable.
-
Propose Features:
- Have a great idea for a new feature? Open a feature request issue in the same GitHub Issues page.
- Describe the feature in detail and explain how it will benefit the project.
-
Submit Pull Requests:
- Fork the repository and create a new branch for your changes.
- Make your modifications and test thoroughly.
- Open a pull request against the
develbranch of the original repository. Include a clear description of your changes and any relevant context.
- Website: https://danielhand.io
- Github: @dsgnr
See the LICENSE file for more details on terms and conditions.