Dieser Artikel zeigt, wie du Pi-hole als rootless Container mit Podman Quadlets auf dem Raspberry Pi Zero 2 W deployst. Die Ansible-Rolle head1328.pihole übernimmt dabei die komplette Konfiguration inkl. Caddy Reverse Proxy, automatischen Container-Updates und iptables Port-Redirection.
Voraussetzung: ein funktionierendes Podman-Setup wie im ersten Artikel der Serie beschrieben.
Architektur#
Web-Traffic:
flowchart TB
A[Browser] --> B["https://pi-hole-primary.fritz.box"]
B --> C["iptables REDIRECT
443 → 8443"]
C --> D["Caddy
internes TLS, Port 8443"]
D --> E["http://pihole:80
Podman Network"]
E --> F[Pi-hole Admin UI]
A ~~~ F
DNS-Traffic:
flowchart LR
A[DNS-Anfragen] --> B["iptables REDIRECT
53 → 5353"]
B --> C["Pi-hole Container
Port 5353"]
Da rootless Podman in der Standardkonfiguration keine privilegierten Ports (< 1024) binden kann, übernimmt iptables die Weiterleitung von Port 53 und 443 auf die Container-Ports 5353 und 8443.
Was ist ein Quadlet?#
Podman 5 bringt native Quadlet-Unterstützung mit. Quadlets sind systemd-Unit-Dateien, die Container deklarativ beschreiben. Der podman-system-generator wandelt diese beim systemd-Reload automatisch in lauffähige Services um.
Vorteile gegenüber podman generate systemd:
- Deklarativ statt imperativ
- Keine generierten Unit-Dateien mehr
- Native systemd-Integration
- Automatische Updates via
AutoUpdate=registry
Ansible-Projekt erweitern#
Das komplette Projekt findest du hier:
Dieses Tutorial basiert auf der Version 1.0.0 .
requirements.yml ergänzen#
---
collections:
- name: devsec.hardening
version: ">=10.4.0,<10.5"
type: galaxy
- name: containers.podman
version: ">=1.18.0,<1.19"
type: galaxy
- name: community.general # NEU
version: ">=10.0.0,<11.0" # NEU
type: galaxy # NEU
roles:
- name: head1328.podman
version: "v0.1.0"
- name: head1328.pihole # NEU
version: "v0.1.0" # NEU
- name: escalate.swap
version: "v2.1.0"Die community.general Collection wird für die iptables-Persistierung benötigt.
ansible-galaxy install -r requirements.ymlInventory erweitern: hosts.yml#
---
all:
vars:
ansible_python_interpreter: /usr/bin/python3
children:
podmen:
children: # NEU
piholes: # NEU
hosts: # NEU
pi-hole-primary: # NEU
ansible_host: pi-hole-primary.fritz.box # NEUDie piholes-Gruppe ist eine Untergruppe von podmen, sodass beide Rollen angewendet werden.
Group Vars: piholes.yml#
---
pihole_image: "docker.io/pihole/pihole:latest"
pihole_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
pihole_timezone: "Europe/Berlin"
pihole_dns_upstreams: "5.1.66.255;185.150.99.255"
pihole_fqdn: pi-hole-primary.fritz.box
pihole_in_interface: eth0
pihole_dns_port: 5353
pihole_https_port: 8443
pihole_autoupdate_enabled: true
pihole_autoupdate_time: "03:00"
pihole_autoupdate_random_delay: 900Das Pi-hole-Passwort verschlüsseln:
# interactive mode (empfohlen)
ansible-vault encrypt_string
# Alternative
ansible-vault encrypt_string 'dein-pi-hole-passwort' --name pihole_passwordDNS Upstreams#
Im Beispiel werden die Freifunk München (FFMUC) Resolver als DNS-Upstream verwendet, datenschutzfreundliche, nicht-kommerzielle DNS-Server der Freifunk-Community. Auch andere Freifunk-Communities in verschiedenen Städten und Ballungsräumen bieten ähnliche Dienste an. Große kommerzielle Anbieter werden hier bewusst nicht genannt.
Playbook erweitern#
play_pihole.yml#
---
- name: Deploy Pi-hole
hosts: piholes
become: true
pre_tasks:
- name: Disable avahi-daemon for Pi-hole
block:
- name: Stop avahi-daemon service
ansible.builtin.systemd:
name: avahi-daemon
state: stopped
enabled: false
- name: Assert podman binary is available
ansible.builtin.command: podman --version
register: podman_version
changed_when: false
failed_when: podman_version.rc != 0
roles:
- role: head1328.piholeDer avahi-daemon wird deaktiviert, da er Port 5353 belegt. Auf einem dedizierten Pi-hole Host wird mDNS nicht benötigt, es gibt keine Drucker, AirPlay-Geräte oder andere Dienste die per Zero-Configuration entdeckt werden müssten. Die lokale Namensauflösung übernimmt im FritzBox-Netzwerk ohnehin die FritzBox über .fritz.box Domains.
In play.yml importieren#
Am Ende der play.yml importierst du das Pi-hole Playbook:
- name: Import Pi-hole playbook
import_playbook: play_pihole.ymlWas die head1328.pihole Rolle macht#
| Schritt | Beschreibung |
|---|---|
| Quadlet-Verzeichnis | ~/.config/containers/systemd für rootless Quadlets |
| Podman Network | Internes Netzwerk internal für Container-Kommunikation |
| Pi-hole Quadlet | Container-Definition mit DNS-Port und Volumes |
| Caddy Quadlet | Reverse Proxy mit internem TLS |
| Caddyfile | Template für HTTPS-Konfiguration |
| systemd Services | Quadlets als User-Services starten |
| Auto-Update Timer | podman-auto-update.timer konfigurieren |
| iptables | Port-Redirection 53→5353, 443→8443 (IPv4/IPv6) |
Das Pi-hole Quadlet#
Die Rolle erstellt ein Quadlet unter ~/.config/containers/systemd/pihole.container:
[Container]
ContainerName=pihole
Image=docker.io/pihole/pihole:latest
Network=internal
PublishPort=5353:53/tcp
PublishPort=5353:53/udp
Environment=TZ=Europe/Berlin
Environment=FTLCONF_dns_upstreams=5.1.66.255;185.150.99.255
Environment=FTLCONF_dns_listeningMode=all
Environment=FTLCONF_webserver_api_password=...
AddCapability=SYS_NICE
Volume=pihole-etc-pihole:/etc/pihole:Z
Volume=pihole-etc-dnsmasq.d:/etc/dnsmasq.d:Z
AutoUpdate=registry
[Install]
WantedBy=default.targetDas Caddy Quadlet#
Caddy übernimmt TLS-Terminierung mit einem internen Zertifikat ~/.config/containers/systemd/caddy.container:
[Container]
ContainerName=caddy
Image=docker.io/caddy:latest
Network=internal
PublishPort=8443:443/tcp
Volume=/etc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,Z
Volume=caddy-data:/data:Z
Volume=caddy-config:/config:Z
AutoUpdate=registry
[Install]
WantedBy=default.targetDas Caddyfile#
Die config (/etc/caddy/Caddyfile) wird in den Container ebenfalls nach /etc/caddy/Caddyfile gemounted:
sudo -u podman bash -c 'cd "$HOME" && podman exec -it caddy sh -c "cat /etc/caddy/Caddyfile"'pi-hole-primary.fritz.box {
tls internal
encode gzip
@root path /
redir @root /admin 302
reverse_proxy pihole:80
}Playbook ausführen#
ansible-playbook play.ymlNach dem Playbook-Lauf ist Pi-hole unter https://pi-hole-primary.fritz.box erreichbar. Damit alle Geräte im Heimnetz Pi-hole nutzen, musst du die IP-Adresse des Pi-hole in der FritzBox als lokalen DNS-Server eintragen.
TLS-Zertifikat vertrauen#
Caddy verwendet eine interne Certificate Authority. Um Browser-Warnungen zu vermeiden, musst du das Root-Zertifikat einmalig exportieren und auf den Client-Geräten als vertrauenswürdig markieren.
Zertifikat exportieren#
ssh ansible@pi-hole-primary.fritz.box
sudo -u podman bash -c 'cd "$HOME" && podman cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root-ca.crt && chmod a+rw /tmp/caddy-root-ca.crt'Auf macOS vertrauen#
scp ansible@pi-hole-primary.fritz.box:/tmp/caddy-root-ca.crt .
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain caddy-root-ca.crtInstallation verifizieren#
Container-Status prüfen#
ssh ansible@pi-hole-primary.fritz.box
sudo -u podman bash -c 'cd "$HOME" && podman ps'Erwartete Ausgabe:
CONTAINER ID IMAGE STATUS NAMES
abc123 docker.io/pihole/pihole:latest Up 10 minutes pihole
def456 docker.io/caddy:latest Up 10 minutes caddysystemd Services prüfen#
sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status pihole.service'
sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status caddy.service'iptables-Regeln prüfen#
sudo iptables -t nat -L PREROUTING -n -v
sudo ip6tables -t nat -L PREROUTING -n -vAutomatische Updates#
Die Rolle konfiguriert podman-auto-update für automatische Container-Updates.
Timer-Status prüfen#
sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status podman-auto-update.timer'
sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user list-timers'Manuelles Update#
# Dry-run
sudo -u podman bash -c 'cd "$HOME" && podman auto-update --dry-run'
# Update durchführen
sudo -u podman bash -c 'cd "$HOME" && podman auto-update'Update-Zeitpunkt anpassen#
In host_vars/pi-hole-primary.yml:
pihole_autoupdate_time: "03:00"
pihole_autoupdate_random_delay: 900Der Random-Delay verhindert, dass mehrere Pi-holes gleichzeitig updaten, wichtig für Hochverfügbarkeit bei Primary/Secondary-Setups.
Fazit#
Mit der head1328.pihole Ansible-Rolle läuft Pi-hole als rootless Container auf dem Raspberry Pi:
- Quadlets für deklarative Container-Definition
- Caddy als Reverse Proxy mit internem TLS
- iptables für Port-Redirection ohne privilegierte Ports
- Automatische Updates via podman-auto-update Timer
- Freifunk München DNS als datenschutzfreundlicher Upstream
Im nächsten Artikel konfigurieren wir ein Primary/Secondary Pi-hole Setup mit FritzBox-Integration und WireGuard VPN.
Verwendete Projekte#
- Pi-hole - DNS-basierter Werbe- und Tracker-Filter
- Caddy - Reverse Proxy mit automatischem TLS
- Podman - daemonless Container-Engine
- Ansible - Automatisierung und Configuration Management
- Freifunk München (FFMUC) - datenschutzfreundliche DNS-Resolver
Eigene Rollen und Playbooks#
head1328.podman(Rolle): Codeberg · Galaxyhead1328.pihole(Rolle): Codeberg · Galaxy- Komplettes Tutorial-Playbook: Codeberg
Hat dir das hier geholfen?
Unterstütze gerne via:
