Technitium DNS Server mit Docker Compose: eigener DNS für das Firmennetz
Technitium DNS Server ist autoritativer und rekursiver DNS-Server mit eigenen Zonen und Filterlisten. Die Anleitung zeigt Docker Compose, interne Zonen, API, Updates und einen im Test reproduzierten Backup-Fallstrick.
Mit KI erstellt – redaktionelle Prüfung ausstehend
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Pi-hole und AdGuard Home filtern Werbung, verwalten aber keine eigenen Zonen. Technitium DNS Server löst eigene Namen wie nas.firma.local auf, ist autoritativ und rekursiv, hat Filterlisten und läuft als einzelner Container.
Vorweg der wichtigste Fallstrick: Ein Backup direkt nach dem Anlegen einer Zone kann unvollständig sein (eigener Abschnitt unten).
Was Technitium DNS Server ist und wo die Grenzen liegen
Technitium DNS Server (GPL-3.0) ist rekursiver Resolver, autoritativer Server für eigene Zonen wie firma.local und Filter mit Blocklisten. Laut Projektseite kommen DHCP, DNS-over-HTTPS, DNS-over-TLS, DNSSEC und API hinzu. Pi-hole und AdGuard Home bieten keine Zonenverwaltung und keine Zonentransfers; für reines Werbefiltern genügen sie.
Grenzen: Fällt der Server aus, verlieren Clients mit nur diesem Resolver die Namensauflösung. Planen Sie einen zweiten Resolver ein.
v15.5.1 (26.09.2026) schloss eine Lücke, über die ein Benutzer mit geringen Rechten Sitzungstoken anderer ermitteln konnte. v15.6 (03.10.2026) behebt weitere Sicherheitsprobleme (Log-Injection im Login-Aufruf, zwei Überlastungsfehler durch authentifizierte Benutzer) und fügt in den Einstellungen eine Option für das Cache-Prefetching hinzu. Setzen Sie 15.6.0 oder neuer ein.
Voraussetzungen und Ressourcen
- Linux-Host mit Docker und Compose-Plugin (amd64 und arm64 laut Docker Hub verfügbar).
- Freier Port 53 (UDP und TCP) und TCP-Port 5380.
- Ein Verzeichnis für die persistenten Daten (klein).
- Zum Ausprobieren genügt ein Rechner mit manuell gesetztem DNS.
Port 53 prüfen: Produktivbetrieb und Testfall
Prüfen Sie vor dem Start, ob Port 53 frei ist.
$ ss -lnup | grep ':53'
$ ss -lntp | grep ':53'127.0.0.53:53 mit systemd-resolve ist der Stub-Resolver (auf Ubuntu üblich).
- Option 1, Stub-Listener abschalten:
DNSStubListener=noin/etc/systemd/resolved.conf, Dienst neu starten. - Option 2, Host-IP binden:
"192.168.10.5:53:53/udp"statt"53:53/udp". - Option 3, abweichender Hostport: der Testfall, DNS auf
15353. Nur fürdig -p, nicht für Clients.
Für den Produktivbetrieb liefern beide ss-Befehle auf der vorgesehenen Adresse keine Zeile mit :53 mehr.
Vollständige compose.yaml und .env
Die vollständige Datei mit Tag 15.6.0 und Port 53.
services:
dns-server:
container_name: technitium-dns
image: technitium/dns-server:15.6.0
hostname: dns-server
ports:
- "5380:5380/tcp"
- "53:53/udp"
- "53:53/tcp"
environment:
- DNS_SERVER_DOMAIN=dns.firma.local
- DNS_SERVER_ADMIN_PASSWORD=${DNS_ADMIN_PASSWORD}
- DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380
volumes:
- ./config:/etc/dns
restart: unless-stoppedDas Passwort steht in der .env. Sie gehört nicht in die Versionskontrolle.
# Datei .env im selben Verzeichnis wie die compose.yaml
# Der Wert hier ist ein Platzhalter, bitte vor dem Start ersetzen
DNS_ADMIN_PASSWORD=IHR_PASSWORT$ echo ".env" >> .gitignore
$ chmod 600 .env| Parameter | Bedeutung und Begründung |
|---|---|
container_name: technitium-dns | Fester Name für docker logs. |
image: technitium/dns-server:15.6.0 | Feste Version statt latest, Updates nur bewusst nach Backup. |
hostname: dns-server | Hostname in Ausgaben und Logs. |
"5380:5380/tcp" | Weboberfläche und API. |
"53:53/udp" | Der DNS-Dienst. |
"53:53/tcp" | Für große Antworten und Zonentransfers. |
DNS_SERVER_DOMAIN=dns.firma.local | Name des Servers, auch im NS-Eintrag eigener Zonen. |
DNS_SERVER_ADMIN_PASSWORD=${DNS_ADMIN_PASSWORD} | Passwort von admin beim ersten Start. Ohne die Variable gilt ein Standardpasswort. |
DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380 | Port im Container, muss zum Portmapping passen. |
./config:/etc/dns | Alle persistenten Daten. Ohne Volume sind Zonen nach docker compose down weg. |
restart: unless-stopped | Start nach Hostneustart, nicht nach manuellem Stopp. |
Bei belegtem Port 53 ändern Sie nur die linken Portzahlen:
ports:
- "5380:5380/tcp"
- "15353:53/udp"
- "15353:53/tcp"Das offizielle Beispiel enthält seit 15.5 einen healthcheck auf /api/dnsClient/healthCheck. Wie Healthchecks in Compose funktionieren, zeigt die Anleitung Docker Compose absichern: Secrets, Healthchecks, Non-Root und Read-Only.
docker compose config läuft ohne Fehlermeldung, ls -l .env zeigt -rw-------.
Installation und Start
Legen Sie beide Dateien im neuen Verzeichnis ab und prüfen Sie sie vor dem Start.
$ mkdir -p ~/technitium-dns
$ cd ~/technitium-dns
$ mkdir -p config$ docker compose configBei DNS_SERVER_ADMIN_PASSWORD muss Ihr Passwort stehen. Ein leerer Wert heißt: .env nicht gefunden.
$ docker compose up -d
$ docker compose ps
$ docker compose logs --tail 50 dns-serverDer Container ist danach Up, das Log enthält:
Using config folder: /etc/dns
Technitium DNS Server was started successfully.docker compose ps zeigt Up, das Log enthält Technitium DNS Server was started successfully.
Funktionsprüfung und Healthcheck
Weboberfläche: Der Statuscode taugt auch für eine Überwachung. Die Antwort lautet HTTP 200.
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5380/Rekursion: Die Abfrage liefert IPv4-Adressen. Auf Port 53 lassen Sie -p 15353 weg.
$ dig @127.0.0.1 -p 15353 example.com +shortOhne healthcheck zeigt Docker nur Up, nicht healthy.
curl liefert 200, dig mindestens eine IP-Adresse.
Erstkonfiguration über Oberfläche und API
Öffnen Sie http://192.168.10.5:5380/ und melden Sie sich als admin mit dem Passwort aus der .env an. Für Skripte dient die API, zuerst der Login (Antwort: JSON mit Token aus 64 Zeichen).
$ curl -s "http://127.0.0.1:5380/api/user/login?user=admin&pass=IHR_PASSWORT&includeInfo=true"Sicherheitshinweis: Passwort und Token laufen als URL-Parameter und landen in Shell-History und Proxy-Logs. Nutzen Sie die API nur über 127.0.0.1 oder ein vertrauenswürdiges Netz und legen Sie das Token in eine Variable.
$ TOKEN="hier-das-token-aus-der-login-antwort"
$ curl -s "http://127.0.0.1:5380/api/zones/list?token=$TOKEN"| Endpunkt | Zweck und wichtige Parameter |
|---|---|
/api/user/login | Anmeldung mit user, pass, includeInfo=true, liefert das Token. |
/api/zones/create | Legt eine Zone an: zone, type=Primary. |
/api/zones/records/add | Fügt einen Record hinzu: domain, type, ttl, ipAddress. overwrite=true ersetzt gleichartige Einträge. |
/api/zones/list | Listet Zonen, schnellste Existenzprüfung nach einem Restore. |
/api/settings/set | Setzt Servereinstellungen, etwa die Blocklisten-URLs. |
/api/settings/backup | Exportiert Konfiguration und Zonen als ZIP. |
/api/settings/restore | Spielt ein Backup zurück, POST mit Multipart-Feld fileUpload. |
Der Login liefert "status":"ok" mit Token, /api/zones/list ebenfalls "status":"ok".
Eigene interne Zone und Records anlegen
Die folgenden Aufrufe legen die Primary-Zone firma.local mit einem A-Record für das NAS an. Beide melden ok, der zweite zusätzlich soaSerial 2 (steigt bei jeder Änderung).
$ curl -s "http://127.0.0.1:5380/api/zones/create?token=$TOKEN&zone=firma.local&type=Primary"$ curl -s "http://127.0.0.1:5380/api/zones/records/add?token=$TOKEN&domain=nas.firma.local&type=A&ttl=3600&ipAddress=192.168.10.20&overwrite=true"Die Gegenprobe liefert 192.168.10.20. Bleibt die Abfrage leer, fragt der Client einen anderen Resolver: Geben Sie den Server mit @ und gegebenenfalls -p an.
$ dig @127.0.0.1 -p 15353 nas.firma.local +short.local ist für mDNS reserviert. Für Dauerbetrieb eignet sich eine Subdomain einer eigenen Domain, etwa intern.ihredomain.de.
dig liefert 192.168.10.20, die Zone steht in /api/zones/list.
Filterlisten aktivieren
Blocklisten sind Textdateien mit Domainnamen, die der Server regelmäßig lädt. In der Weboberfläche tragen Sie unter Settings eine URL pro Zeile ein, per API so:
$ curl -s "http://127.0.0.1:5380/api/settings/set?token=$TOKEN&blockListUrls=https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"Der Aufruf liefert HTTP 200. Legen Sie früh eine Ausnahmeliste an.
Settings zeigt die Blocklisten-URL, das Dashboard nach der ersten Aktualisierung die Zahl blockierter Domains.
Persistente Daten und das Rechteproblem beim Aufräumen
Alle Daten liegen in ./config:
auth.config: Benutzer;dns.config: Servereinstellungenallowed.config,blocked.config,blocklist.config: Ausnahmen, manuelle Sperren, Blocklistenscopes/Default.scope: DHCP-Bereich, ungenutztzones/firma.local.zone: eigene Zone, erst nach Verzögerung (nächster Abschnitt)
Der Container läuft als root, alles in ./config gehört danach root. Ein einfaches rm -rf scheitert mit Permission denied. Ausweg: ein Wegwerf-Container mit gemountetem Pfad (oder sudo).
$ ls -la config
$ docker run --rm -v "$PWD/config:/data" alpine sh -c "rm -rf /data/*"
$ rmdir configls -la config zeigt die Dateien mit Eigentümer root.
Backup und Restore
Der Export liefert HTTP 200 und eine gültige ZIP-Datei. Dann den Inhalt prüfen:
$ curl -s -o backup.zip "http://127.0.0.1:5380/api/settings/backup?token=$TOKEN&blockLists=true&logs=false&scopes=true&apps=true&stats=true&zones=true&allowedZones=true&blockedZones=true&dnsSettings=true&logSettings=true&authConfig=true"
$ unzip -l backup.zipRestore:
$ curl -s -X POST -F "fileUpload=@backup.zip" "http://127.0.0.1:5380/api/settings/restore?token=$TOKEN&deleteExistingFiles=true&blockLists=true&logs=false&scopes=true&apps=true&stats=true&zones=true&allowedZones=true&blockedZones=true&dnsSettings=true&logSettings=true&authConfig=true"Die Datei muss per -F als Feld fileUpload kommen. deleteExistingFiles=true entfernt vorhandene Dateien vorher und ist destruktiv: Was im Backup fehlt, ist danach weg.
Gegenprobe: Ist die Zone gelöscht, bleibt die Abfrage unbeantwortet. Nach dem Einspielen des Backups (HTTP 200) steht die Zone wieder in der Liste und dig liefert 192.168.10.20.
$ curl -s "http://127.0.0.1:5380/api/zones/list?token=$TOKEN"
$ dig @127.0.0.1 -p 15353 nas.firma.local +shortDie Zonenliste enthält firma.local, dig liefert 192.168.10.20.
Fallstrick: das unvollständige Backup direkt nach einer Änderung
Direkt nach dem Anlegen der Zone war config/zones/ leer. Ein Backup in diesem Moment enthielt sechs Dateien, keine Zonendatei. Erst rund 20 Sekunden später lag config/zones/firma.local.zone auf der Platte, das danach erzeugte Backup enthielt sie (sieben Dateien).
Ein Restore aus dem ersten Backup brachte die Zone nicht zurück, obwohl Backup und Restore HTTP 200 meldeten. Ursache: Der Server hält Zonenänderungen zunächst im Speicher und schreibt verzögert, der Export packt den Stand der Platte.
- Nie unmittelbar nach einer Änderung sichern: mindestens eine Minute Abstand.
- Jedes Backup prüfen:
unzip -lzeigt, obzones/enthalten ist.
unzip -l backup.zip | grep 'zones/'
if ! unzip -l backup.zip | grep -q 'zones/'; then
echo "FEHLER: Backup enthält keine Zonendateien, nicht als gültig ablegen"
exit 1
fiunzip -l backup.zip | grep 'zones/' listet je eigene Zone eine Datei zones/<zone>.zone.
Sichere Netzwerkfreigabe
Ein rekursiver Resolver, der Anfragen aus dem Internet beantwortet, ist ein offener Resolver und dient Verstärkungsangriffen. Geben Sie Port 53 nie ins Internet frei, binden Sie ihn an eine interne Adresse, beschränken Sie per Firewall und legen Sie in Technitium fest, welche Netze rekursiv fragen dürfen.
$ ss -lntp | grep -E '5380|:53'
$ dig @192.168.10.5 -p 15353 example.com +shortPort 5380 läuft unverschlüsselt über HTTP, Anmeldedaten und Token gehen im Klartext. Stellen Sie einen Reverse Proxy mit TLS davor und binden Sie die Oberfläche an localhost, oder nutzen Sie SSH-Tunnel oder Management-VLAN.
Eine rekursive Abfrage von außerhalb der erlaubten Netze wird abgelehnt (status: REFUSED), die Oberfläche ist von außen nicht per HTTP erreichbar.
Updates und Rollback-Grenzen
Beim Anheben des Tags von 15.5.1 auf 15.6.0 bleibt eine zuvor angelegte Zone samt A-Record unverändert vorhanden, die Weboberfläche antwortet mit 200 und meldet Version 15.6. Im Betrieb setzen Sie den neuen festen Tag.
$ curl -s -o backup-vor-update.zip "http://127.0.0.1:5380/api/settings/backup?token=$TOKEN&zones=true&dnsSettings=true&authConfig=true&blockLists=true&scopes=true&allowedZones=true&blockedZones=true&logSettings=true&stats=true&apps=true&logs=false"
$ unzip -l backup-vor-update.zip | grep 'zones/'
$ docker compose pull
$ docker compose up -d
$ docker compose logs --tail 30 dns-serverZone und A-Record überlebten den Wechsel, weil die Daten in ./config liegen.
Das Log zeigt Technitium DNS Server was started successfully., die Zone wird weiter aufgelöst, die Weboberfläche nennt unten die neue Version.
Das Datenformat kann migrieren. Nutzen Sie für den Rückweg das Backup der alten Version.
Typische Fehler mit Diagnose und Lösung
1. Port 53 ist belegt (address already in use). Prüfung und Optionen stehen im Abschnitt zu Port 53.
2. Der Container startet immer wieder neu. Die Ursache steht meist im Log, oft Rechte am Datenverzeichnis oder fehlende Variablen. Prüfen Sie docker compose config auf einen Wert bei DNS_SERVER_ADMIN_PASSWORD.
$ docker compose ps
$ docker compose logs dns-server | tail -603. Die Weboberfläche ist nicht erreichbar. Prüfen Sie den Statuscode auf dem Host (siehe Funktionsprüfung). Kommt dort 200, blockiert von außen die Firewall oder das Mapping bindet nur 127.0.0.1. Sonst muss DNS_SERVER_WEB_SERVICE_HTTP_PORT zum Containerport passen.
4. Die eigene Zone wird nicht aufgelöst. Entweder fragt der Client noch den alten Resolver (mit explizitem @ klappt es, ohne nicht), oder Zone oder Record fehlen. /api/zones/list zeigt das sofort.
5. Das Backup ist unvollständig. Abstand halten und mit unzip -l backup.zip | grep 'zones/' prüfen. Ist config/zones trotz Zone leer, kurz warten.
6. Aufräumen scheitert mit Permission denied. Die Dateien gehören root: Wegwerf-Container oder sudo, siehe persistente Daten.
Saubere Deinstallation
Warnung, Datenverlust: Die Befehle löschen config mit allen Zonen, Records, Benutzern und Einstellungen unwiderruflich. Sichern Sie vorher und prüfen Sie das Backup per unzip -l.
# Schritt 1: Container stoppen, Netzwerke und anonyme Volumes entfernen
docker compose down -v --remove-orphans
# Schritt 2: Besitzverhältnisse prüfen, bevor gelöscht wird
ls -la config
# Schritt 3: Inhalt über einen Wegwerf-Container löschen, da root der Besitzer ist
docker run --rm -v "$PWD/config:/data" alpine sh -c "rm -rf /data/*"
# Schritt 4: Leeres Datenverzeichnis entfernen
rmdir config
# Schritt 5: Images namentlich entfernen, nicht per Pauschalbereinigung
docker rmi technitium/dns-server:15.6.0Stellen Sie vorher DNS auf Clients und Router zurück.
Passende Anleitungen auf S-EDV
- DNS-Records erklärt: A, AAAA, CNAME, MX und TXT: Die Grundlagen zu den Recordtypen, die Sie in Ihrer eigenen Zone
firma.localanlegen. - Pi-hole als Werbeblocker per DNS mit Docker einrichten
- Nginx als Reverse Proxy mit TLS einrichten


