Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Server & Netzwerk 24.09.2026 · 25 min Lesezeit

Technitium DNS Server mit Docker Compose: eigener DNS für das Firmennetz

Technitium DNS Server ist ein vollwertiger autoritativer und rekursiver DNS-Server mit eigenen Zonen, nicht nur ein Werbefilter. Diese Anleitung zeigt Schritt für Schritt Installation per Docker Compose, interne Zonen und Records, Filterlisten, API-Nutzung, Updates und einen im Test reproduzierten Fallstrick beim Backup, der Zonen still verschwinden lässt.

Illustration zur Anleitung mit der Überschrift Eigener DNS-Server mit Docker, drei Feature-Karten zu internen Zonen, Filterlisten und Backup-Test sowie einem abstrakten Netzwerk- und Dashboard-Mockup in Blautönen. KI-generiert

Wer im Firmennetz eigene Namen wie nas.firma.local auflösen will und gleichzeitig Werbung und Tracker netzwerkweit filtern möchte, greift oft zu Pi-hole oder AdGuard Home. Beide sind gute Filter, aber keine vollwertigen DNS-Server mit eigenen Zonen. Technitium DNS Server schließt genau diese Lücke: Er ist autoritativ und rekursiv, verwaltet eigene Zonen und Records, bringt Filterlisten mit und läuft als einzelner Docker-Container.

Diese Anleitung beschreibt den kompletten Weg von der compose.yaml über die Erstkonfiguration per Weboberfläche und API bis zu Backup, Restore, Update und sauberer Deinstallation. Alle Schritte wurden am 24.09.2026 in einer eigenen Testumgebung auf einem Ubuntu-Host mit Docker ausgeführt. Der wichtigste Praxisfund dabei: Ein Backup direkt nach dem Anlegen einer Zone kann unvollständig sein und die Zone beim Restore NICHT zurückbringen. Dieser Fallstrick steht weiter unten als eigener Abschnitt.

Was in diesem Artikel getestet wurde und was nicht

Damit klar ist, worauf Sie sich verlassen können, hier die Trennlinie.

In einer eigenen Testumgebung ausgeführt und bestätigt: Start des Stacks per docker compose up -d, Logprüfung, HTTP-200-Antwort der Weboberfläche, rekursive Auflösung per dig, API-Login, Anlegen einer Primary-Zone firma.local, Anlegen eines A-Records nas.firma.local auf 192.168.10.20 und dessen Auflösung, Setzen einer Blocklisten-URL per API, Backup-Export, Restore per Multipart-Upload, der komplette Zyklus aus Zone löschen und per Restore wiederherstellen, der Wechsel des Image-Tags von 15.5.0 auf latest, sowie das Rechteproblem beim Aufräumen des Bind-Mount-Verzeichnisses.

Nicht getestet, alle Angaben dazu stammen aus der offiziellen Dokumentation und sind im Text so gekennzeichnet: DNS-over-HTTPS und DNS-over-TLS mit einem echten Zertifikat, Betrieb direkt auf Hostport 53 (im Test war der Hostport 15353 gemappt), die DHCP-Funktion, Cluster- und Secondary-Zonen, sowie der Betrieb hinter einem Reverse Proxy.

Was Technitium DNS Server ist und wo die Grenzen liegen

Technitium DNS Server ist ein quelloffener DNS-Server unter GPL-3.0, der drei Rollen in einem Dienst vereint. Er ist ein rekursiver Resolver, löst also Namen im Internet für Ihre Clients auf. Er ist ein autoritativer Server, verwaltet also eigene Zonen wie firma.local mit eigenen Records. Und er bringt eine Filterebene mit, die Werbe- und Trackerdomains über Blocklisten aussperrt. Laut offizieller Projektseite gehören außerdem ein DHCP-Server, DNS-over-HTTPS, DNS-over-TLS, DNSSEC und eine Anbindung per API zum Funktionsumfang.

Der Unterschied zu Pi-hole und AdGuard Home ist deshalb kein Detail, sondern der Kern der Werkzeugwahl. Pi-hole und AdGuard Home sind in erster Linie filternde Weiterleiter. Sie kennen Blocklisten und einzelne lokale Einträge, aber sie sind keine autoritativen Server für ganze Zonen mit vollständiger Recordverwaltung, Zonentransfers oder SOA-Serialpflege. Wenn Sie im Firmennetz interne Namen für Server, NAS, Drucker und Dienste pflegen wollen und dafür bisher Hosts-Dateien verteilt haben, ist Technitium das passendere Werkzeug. Wenn Sie ausschließlich Werbung filtern wollen, sind die schlankeren Alternativen völlig ausreichend.

Die Grenzen sollten Sie vorher kennen. Ein DNS-Server ist ein zentraler Dienst: Fällt er aus, ist für die Clients, die ihn als einzigen Resolver eingetragen haben, das Netz praktisch tot. Planen Sie deshalb von Anfang an einen zweiten Resolver ein, sei es ein zweiter Technitium oder der Router. Die Weboberfläche läuft im Standard unverschlüsselt über HTTP. Und der Container läuft als root, was beim Aufräumen des Datenverzeichnisses zu einem konkreten Problem führt, das weiter unten beschrieben ist.

Zum Projektstand, abgerufen über die GitHub-API am 24.09.2026: Das Repository TechnitiumSoftware/DnsServer hat 9.981 Sterne, der letzte Push erfolgte am 20.09.2026, das Repository ist nicht archiviert und steht unter GPL-3.0. Das aktuellste Release ist v15.5.0 vom 19.09.2026. Diese Zahlen belegen aktive Pflege und eine breite Nutzerbasis zum Abrufdatum. Eine Aussage über Trends oder Wachstum lässt sich daraus nicht ableiten und wird hier deshalb nicht getroffen.

Voraussetzungen und Ressourcen

  • Ein Linux-Host mit Docker und dem Compose-Plugin. Im Test war das ein Ubuntu-Host mit docker compose als Subkommando, nicht das alte docker-compose-Binary.
  • Ein freier Port 53 auf UDP und TCP, oder ein bewusster Plan, wie Sie damit umgehen. Auf vielen Ubuntu-Installationen ist Port 53 durch systemd-resolved belegt. Wie Sie das prüfen, steht im nächsten Abschnitt.
  • Ein freier TCP-Port für die Weboberfläche. Der Standard ist 5380.
  • Ein Verzeichnis für die persistenten Daten. Der Inhalt ist klein, im Test lagen nach der Erstkonfiguration sechs bis sieben Konfigurations- und Zonendateien dort. Nennenswert wächst er erst durch Logs und große Blocklisten. Eine belastbare Zahl für Ihren Betrieb lässt sich daraus nicht ableiten, planen Sie mit Reserve und beobachten Sie das Verzeichnis.
  • Einen Plan, welche Clients den Server als Resolver bekommen. Für einen ersten Test reicht ein einzelner Rechner mit manuell gesetztem DNS.

Zur getesteten Umgebung: Image technitium/dns-server:15.5.0 auf einem Ubuntu-Host mit Docker, anschließend Wechsel auf technitium/dns-server:latest. Laut Docker Hub stellt das Projekt Images für mehrere Architekturen bereit, darunter amd64 und arm64. Getestet wurde hier ausschließlich auf amd64.

Port 53 prüfen: Produktivbetrieb und Testfall

Bevor Sie den Stack starten, prüfen Sie, ob Port 53 überhaupt frei ist. Zwei Befehle genügen, einer für UDP, einer für TCP.

# Lauschende UDP-Sockets anzeigen und nach Port 53 filtern
ss -lnup | grep ':53'
# Lauschende TCP-Sockets anzeigen und nach Port 53 filtern
ss -lntp | grep ':53'

Kommt hier eine Zeile mit 127.0.0.53:53 und einem Prozessnamen wie systemd-resolve zurück, ist der Port durch den lokalen Stub-Resolver belegt. Das ist auf Ubuntu-Systemen der Normalfall. Hinweis zur Belegbarkeit: Die folgenden drei Optionen sind dokumentiertes Standardvorgehen, sie wurden in diesem Test nicht durchgespielt. Im Test lief der Container bewusst auf abweichenden Hostports.

  • Option 1, den Stub-Listener abschalten: In /etc/systemd/resolved.conf wird DNSStubListener=no gesetzt und der Dienst neu gestartet. Danach ist Port 53 frei, die Namensauflösung des Hosts selbst muss dann über /etc/resolv.conf sauber weiterhin funktionieren.
  • Option 2, den Container an eine bestimmte Host-IP binden: Statt "53:53/udp" wird "192.168.10.5:53:53/udp" gemappt. Der Stub-Listener auf 127.0.0.53 bleibt dann unberührt.
  • Option 3, ein abweichender Hostport für den Test: Genau das war der Testfall. Die DNS-Ports wurden auf 15353 gemappt, die Weboberfläche auf 5380. Für einen Funktionstest per dig reicht das völlig, weil dig den Port mit -p annehmen kann. Für den Produktivbetrieb ist es untauglich, weil normale Clients DNS immer auf Port 53 erwarten.

Vollständige compose.yaml und .env

Das ist die Datei, die im Test ausgeführt wurde. Sie zeigt die produktive Variante mit Port 53. Für den Testfall mit belegtem Port 53 ändern Sie nur die linke Seite der beiden DNS-Portzeilen, wie im Abschnitt darunter erklärt.

services:
  dns-server:
    container_name: technitium-dns
    image: technitium/dns-server:15.5.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-stopped

Das Passwort steht nicht in der Compose-Datei, sondern in einer .env im selben Verzeichnis. Docker Compose liest sie automatisch und ersetzt ${DNS_ADMIN_PASSWORD}.

# Datei .env im selben Verzeichnis wie die compose.yaml
# Der Wert hier ist ein Platzhalter, bitte vor dem Start ersetzen
DNS_ADMIN_PASSWORD=BitteHierEinLangesPasswortSetzen

Die .env gehört nicht ins Versionskontrollsystem. Wenn das Verzeichnis unter Git liegt, tragen Sie .env in die .gitignore ein, bevor Sie das erste Mal committen. Ein einmal eingecheckter Wert bleibt in der Historie stehen, auch wenn Sie ihn später löschen. Setzen Sie außerdem die Rechte auf nur für den Besitzer lesbar.

# .env von der Versionskontrolle ausschliessen
echo ".env" >> .gitignore
# Leserechte auf den Besitzer beschraenken
chmod 600 .env

Jetzt jeder Parameter im Einzelnen.

ParameterBedeutung und Begründung
container_name: technitium-dnsFester Containername. Erleichtert Befehle wie docker logs technitium-dns, weil Sie nicht den von Compose generierten Namen raten müssen.
image: technitium/dns-server:15.5.0Feste Version statt latest. Das ist der wichtigste Punkt der ganzen Datei. Mit einem gepinnten Tag wissen Sie jederzeit, welche Version läuft, ein docker compose pull zieht nicht unbemerkt eine neue Hauptversion nach, und ein Rollback ist wenigstens überhaupt denkbar. Updates werden damit zu einer bewussten Entscheidung, bei der Sie vorher ein Backup ziehen.
hostname: dns-serverSetzt den Hostnamen innerhalb des Containers. Der Server verwendet ihn in eigenen Ausgaben und Logs, was die Zuordnung erleichtert, wenn mehrere Instanzen laufen.
"5380:5380/tcp"Die Weboberfläche und die API. Links der Hostport, rechts der Containerport. Beide gleich zu halten vermeidet Verwirrung, weil die im Container konfigurierte Portnummer dann zur URL passt.
"53:53/udp"Der eigentliche DNS-Dienst. UDP ist der Normalfall für DNS-Abfragen. Ohne diese Zeile beantwortet der Server keine gewöhnliche Anfrage.
"53:53/tcp"DNS über TCP. Nötig für Antworten, die nicht in ein UDP-Paket passen, und für Zonentransfers. Diese Zeile wird oft vergessen, mit der Folge, dass große Antworten scheitern, während kleine funktionieren.
DNS_SERVER_DOMAIN=dns.firma.localDer eigene Name des Servers. Er erscheint in der Oberfläche und wird für den NS-Eintrag eigener Zonen verwendet. Passen Sie ihn an Ihre interne Namensstruktur an.
DNS_SERVER_ADMIN_PASSWORD=${DNS_ADMIN_PASSWORD}Setzt das Passwort des Benutzers admin beim ersten Start. Der Wert kommt aus der .env und steht damit nicht in der Compose-Datei. Ohne diese Variable wird ein Standardpasswort verwendet, das Sie in der Oberfläche sofort ändern müssten.
DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380Der Port, auf dem die Weboberfläche im Container lauscht. Muss zur rechten Seite des Portmappings passen, sonst zeigt das Mapping auf einen Port, auf dem nichts antwortet.
./config:/etc/dnsDas Bind-Mount für alle persistenten Daten. /etc/dns ist das Konfigurationsverzeichnis des Servers, im Log bestätigt durch die Zeile Using config folder: /etc/dns. Ohne dieses Volume sind Zonen und Einstellungen beim nächsten docker compose down verloren. Ein Bind-Mount statt eines benannten Volumes hat den Vorteil, dass Sie die Dateien direkt im Dateisystem sehen und sichern können. Nachteil ist das Rechteproblem weiter unten.
restart: unless-stoppedDer Container startet nach einem Neustart des Hosts automatisch wieder, aber nicht, wenn Sie ihn selbst per docker compose stop angehalten haben. Für einen zentralen Dienst wie DNS ist das die richtige Wahl, weil ein Hostneustart nicht das halbe Netz lahmlegen soll.

Für den Testfall mit belegtem Port 53 ändern Sie nur diese beiden Zeilen. Der Containerport rechts bleibt 53, weil der Server intern weiterhin dort lauscht.

    ports:
      - "5380:5380/tcp"
      - "15353:53/udp"
      - "15353:53/tcp"

Installation und Start

Alle Schritte in der Reihenfolge, in der sie im Test ausgeführt wurden. Es wird kein Zwischenschritt übersprungen.

# Arbeitsverzeichnis fuer den Stack anlegen und hineinwechseln
mkdir -p ~/technitium-dns
cd ~/technitium-dns
# Datenverzeichnis fuer die persistente Konfiguration anlegen
mkdir -p config

Legen Sie nun die beiden Dateien compose.yaml und .env mit dem Inhalt aus dem vorigen Abschnitt in ~/technitium-dns ab. Danach prüfen Sie, ob Compose die Datei überhaupt fehlerfrei versteht, bevor Sie starten.

# Compose-Datei samt eingesetzter Variablen pruefen, ohne etwas zu starten
docker compose config

In der Ausgabe muss bei DNS_SERVER_ADMIN_PASSWORD Ihr Passwort aus der .env stehen. Steht dort ein leerer Wert, hat Compose die .env nicht gefunden, meist weil sie im falschen Verzeichnis liegt. Jetzt der Start.

# Image holen und Container im Hintergrund starten
docker compose up -d
# Laufstatus des Stacks anzeigen
docker compose ps
# Startprotokoll der letzten Zeilen ansehen
docker compose logs --tail 50 dns-server

Im Test lieferte der Start einen sauberen Durchlauf. Der Container erreichte den Status Up, und im Log standen unter anderem diese beiden Zeilen, die hier wörtlich aus dem Testprotokoll übernommen sind:

Using config folder: /etc/dns
Technitium DNS Server was started successfully.

Erst wenn die zweite Zeile erscheint, ist der Dienst bereit. Wenn stattdessen eine Fehlermeldung zum Binden eines Ports kommt, springen Sie zum Abschnitt über typische Fehler.

Funktionsprüfung und Healthcheck

Drei Prüfungen, die alle im Test durchgeführt wurden. Prüfen Sie in dieser Reihenfolge, weil jede Stufe auf der vorigen aufbaut.

Erste Prüfung, antwortet die Weboberfläche? Statt im Browser zu klicken, fragen Sie nur den HTTP-Statuscode ab. Das ist auch die Grundlage für eine externe Überwachung.

# Nur den HTTP-Statuscode der Weboberflaeche ausgeben
curl -s -o /dev/null -w "%{http_code}
" http://127.0.0.1:5380/

Im Test antwortete die Oberfläche auf dem gemappten Port 5380 mit HTTP 200. Genau diesen Aufruf können Sie in eine Überwachung übernehmen, ein anderer Code als 200 ist dann ein Alarm.

Zweite Prüfung, löst der Server Namen aus dem Internet auf? Das prüft die rekursive Funktion.

# Rekursive Aufloesung gegen den eigenen Server testen, Testfall mit Port 15353
dig @127.0.0.1 -p 15353 example.com +short

Im Test lieferte dieser Aufruf zwei Adressen zurück: 104.20.23.154 und 172.66.147.243. Wichtig zur Einordnung: Das sind die zum Testzeitpunkt gültigen Antworten für diesen Namen, keine festen Werte. Bei Ihnen können andere Adressen zurückkommen, entscheidend ist, dass überhaupt eine Adresse kommt und nicht eine leere Ausgabe. Wenn Sie produktiv auf Port 53 laufen, lassen Sie -p 15353 weg.

Dritte Prüfung, läuft der Container aus Sicht von Docker?

# Status und Portmappings des Stacks anzeigen
docker compose ps

Ein Hinweis zur Ehrlichkeit: Die compose.yaml oben enthält keinen eigenen healthcheck-Block, und im Test wurde keiner ergänzt. Die Statusanzeige zeigt deshalb Up, aber kein healthy. Die beiden Prüfungen per curl und dig sind hier die inhaltliche Gesundheitsprüfung.

Erstkonfiguration über Oberfläche und API

Für den ersten Zugang öffnen Sie http://SERVER-IP:5380/ im Browser und melden sich als Benutzer admin mit dem Passwort aus der .env an. In der Oberfläche finden Sie die Bereiche für Zonen, Einstellungen, Blocklisten und Protokolle. Für einmalige Einrichtungsschritte ist das der bequemste Weg.

Interessanter für Admins ist die API, weil sich damit Zonen und Records skriptgesteuert pflegen lassen. Die folgenden Aufrufe wurden im Test real ausgeführt. Zuerst der Login, der ein Sitzungstoken zurückgibt.

# Anmeldung an der API, liefert JSON mit dem Token
curl -s "http://127.0.0.1:5380/api/user/login?user=admin&pass=IHR_PASSWORT&includeInfo=true"

Im Test lieferte dieser Aufruf ein JSON-Objekt mit einem Token aus 64 Zeichen. Der Parameter includeInfo=true ergänzt die Antwort um Informationen zum Server und zum angemeldeten Benutzer.

Ein Sicherheitshinweis, der hier wirklich wichtig ist: Sowohl das Passwort beim Login als auch das Token bei allen folgenden Aufrufen werden als URL-Parameter übergeben. Das hat drei konkrete Folgen. Erstens landen sie in der Shell-History Ihres Benutzers, also in ~/.bash_history. Zweitens können sie in Zugriffsprotokollen auftauchen, sowohl auf dem Server selbst als auch auf jedem Proxy dazwischen. Drittens sind sie bei unverschlüsseltem HTTP im Netz mitlesbar. Praktische Gegenmaßnahmen: Setzen Sie das Token in eine Shell-Variable, sprechen Sie die API nur über 127.0.0.1 oder ein vertrauenswürdiges Netz an, und wenn Sie Befehle interaktiv eingeben, stellen Sie ihnen ein Leerzeichen voran, damit die Shell sie bei gesetztem HISTCONTROL=ignorespace nicht speichert.

# Token einmal in eine Variable legen, statt es in jeden Befehl zu schreiben
TOKEN="hier-das-token-aus-der-login-antwort"
# Alle vorhandenen Zonen auflisten
curl -s "http://127.0.0.1:5380/api/zones/list?token=$TOKEN"

Die weiteren im Test verwendeten Endpunkte in der Übersicht.

EndpunktZweck und wichtige Parameter
/api/user/loginAnmeldung mit user, pass und includeInfo=true. Liefert das Token für alle weiteren Aufrufe.
/api/zones/createLegt eine Zone an. Parameter zone für den Namen und type=Primary für eine eigene, selbst verwaltete Zone.
/api/zones/records/addFügt einen Record hinzu. Parameter domain, type, ttl, bei A-Records ipAddress, und overwrite=true, damit ein bestehender gleichartiger Eintrag ersetzt statt zusätzlich angelegt wird.
/api/zones/listListet alle Zonen. Die schnellste Prüfung, ob eine Zone wirklich existiert, besonders nach einem Restore.
/api/settings/setSetzt Servereinstellungen, im Test die Blocklisten-URLs.
/api/settings/backupExportiert Konfiguration und Zonen als ZIP-Datei.
/api/settings/restoreSpielt ein Backup zurück. Erwartet einen POST mit Multipart-Upload im Feld fileUpload.

Eigene interne Zone und Records anlegen

Das ist der Teil, der Technitium von einem reinen Filter unterscheidet. Im Test wurde die Zone firma.local als Primary-Zone angelegt und darin ein A-Record für das NAS gesetzt.

# Eigene interne Zone als Primary-Zone anlegen
curl -s "http://127.0.0.1:5380/api/zones/create?token=$TOKEN&zone=firma.local&type=Primary"

Im Test antwortete der Server mit dem Status ok. Danach der Record für das NAS.

# A-Record fuer das NAS in der eigenen Zone setzen
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"

Auch hier lieferte der Test den Status ok, zusätzlich meldete die Antwort soaSerial 2. Der SOA-Serial zählt bei jeder Änderung an der Zone hoch, er ist also ein brauchbarer Indikator dafür, dass die Änderung wirklich angekommen ist. Jetzt die Gegenprobe per Abfrage.

# Eigenen Record gegen den Server abfragen
dig nas.firma.local +short

Im Test lieferte diese Abfrage 192.168.10.20 zurück. Damit ist die interne Namensauflösung nachweislich funktionsfähig. Wenn Ihre Abfrage leer bleibt, fragt Ihr Client noch einen anderen Resolver, dann geben Sie den Server explizit mit @ und gegebenenfalls -p an.

Zur Wahl des Zonennamens ein Hinweis: firma.local dient hier als Beispiel und ist für einen abgeschlossenen Test unproblematisch. Die Endung .local ist allerdings für mDNS reserviert, was auf Clients mit aktiver mDNS-Auflösung zu Reibungen führen kann. Für eine dauerhafte Installation ist eine Subdomain einer Domain, die Ihnen tatsächlich gehört, die robustere Wahl, etwa intern.ihredomain.de. Welche Recordtypen es gibt und wann Sie welchen brauchen, ist ein eigenes Thema, dazu unten ein passender Verweis.

Filterlisten aktivieren

Die Filterung ist bei Technitium eine Zusatzfunktion, nicht der Hauptzweck. Sie funktioniert über Blocklisten, also Textdateien mit Domainnamen, die der Server in festen Abständen selbst herunterlädt und anwendet.

Über die Weboberfläche gehen Sie in den Bereich Settings und dort zum Abschnitt für Blocklisten. Dort tragen Sie eine URL pro Zeile ein, speichern, und lösen die Aktualisierung aus. Danach zeigt die Oberfläche, wie viele Einträge geladen wurden. Genau das lässt sich auch per API erledigen.

# Blocklisten-URL per API setzen
curl -s "http://127.0.0.1:5380/api/settings/set?token=$TOKEN&blockListUrls=https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"

Im Test antwortete dieser Aufruf mit HTTP 200. Bestätigt ist damit: Der Aufruf wird angenommen und die Einstellung gesetzt. Nicht Teil dieses Tests war eine Messung, wie viele Domains anschließend tatsächlich blockiert wurden, oder ein Vergleich der Filterwirkung mit anderen Lösungen. Solche Aussagen finden Sie hier deshalb nicht.

Zwei Punkte aus der Praxis, die Sie einplanen sollten. Erstens braucht der Server für den Download der Listen Internetzugang, und die erste Aktualisierung dauert bei großen Listen einen Moment. Zweitens filtern Blocklisten pauschal. Rechnen Sie damit, dass einzelne Dienste ungewollt blockiert werden, und legen Sie sich von Anfang an eine Ausnahmeliste an, statt im Störungsfall die ganze Filterung abzuschalten.

Persistente Daten und das Rechteproblem beim Aufräumen

Alle dauerhaften Daten liegen im gemounteten Verzeichnis ./config, das im Container als /etc/dns erscheint. Im Test lagen nach der Erstkonfiguration folgende Dateien darin, hier genau in der im Protokoll festgehaltenen Form:

  • auth.config für Benutzer und Anmeldedaten
  • dns.config für die Servereinstellungen
  • allowed.config für die Ausnahmen von der Filterung
  • blocked.config für manuell blockierte Domains
  • blocklist.config für die konfigurierten Blocklisten
  • scopes/Default.scope für den DHCP-Bereich, der im Test nicht genutzt wurde
  • zones/firma.local.zone für die eigene Zone, aber erst nach einer Verzögerung, siehe den folgenden Abschnitt

Wenn Sie diese Dateien kennen, wissen Sie auch, was ein Backup enthalten muss. Und Sie wissen, worauf es ankommt: Ohne zones/ fehlen Ihre eigenen Zonen.

Jetzt der Punkt, der im Test real zugeschlagen hat. Der Container läuft als root. Alles, was der Server in ./config schreibt, gehört danach dem Benutzer root auf dem Host, obwohl das Verzeichnis ursprünglich von Ihrem normalen Benutzer angelegt wurde. Beim Aufräumen des Testverzeichnisses als normaler Benutzer scheiterte rm -rf deshalb mit Permission denied. Der im Test genutzte Ausweg war ein Wegwerf-Container, der den Pfad mountet und das Löschen mit den passenden Rechten im Container erledigt.

# Besitzverhaeltnisse des Datenverzeichnisses pruefen
ls -la config
# Inhalt ueber einen Wegwerf-Container loeschen, der den Pfad gemountet bekommt
docker run --rm -v "$PWD/config:/data" alpine sh -c "rm -rf /data/*"
# Jetzt laesst sich das leere Verzeichnis als normaler Benutzer entfernen
rmdir config

Der Weg über sudo rm -rf funktioniert natürlich ebenfalls, sofern Sie sudo-Rechte haben. Getestet wurde hier die Variante über den Container, weil sie auch auf Hosts greift, auf denen Sie kein sudo haben, aber Docker bedienen dürfen. Denken Sie in beiden Fällen daran, dass Sie damit alle Zonen und Einstellungen löschen.

Backup und Restore

Der komplette Zyklus wurde im Test durchgespielt, einschließlich der Gegenprobe, dass die Daten nach dem Restore wirklich wieder da sind. Zuerst der Export.

# Vollstaendiges Backup als ZIP-Datei exportieren
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"
# Inhalt des Backups pruefen, ohne es zu entpacken
unzip -l backup.zip

Im Test lieferte der Export HTTP 200 und eine gültige ZIP-Datei. Der zweite Befehl ist keine Höflichkeit, sondern der entscheidende Prüfschritt, warum, steht im nächsten Abschnitt. Nun der Restore.

# Backup per POST mit Multipart-Upload zurueckspielen
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"

Zwei Details sind hier wichtig. Die Datei muss als Multipart-Upload im Feld fileUpload übergeben werden, deshalb -F und nicht -d. Und deleteExistingFiles=true bedeutet, dass vorhandene Dateien vor dem Einspielen entfernt werden. Das ist der saubere Weg für einen echten Wiederherstellungsfall, aber es ist auch eine destruktive Option: Was im Backup fehlt, ist danach weg.

Im Test antwortete der Restore mit HTTP 200. Der Zyklus wurde vollständig reproduziert: Die Zone firma.local wurde gelöscht, eine Abfrage von nas.firma.local lieferte danach keine Antwort mehr, dann wurde das Backup eingespielt. Anschließend enthielt die Zonenliste firma.local wieder, und dig nas.firma.local +short lieferte erneut 192.168.10.20. Genau diese Gegenprobe ist der Punkt: Ein Backup, das Sie nie zurückgespielt haben, ist kein geprüftes Backup.

# Nach dem Restore pruefen, ob die Zone wieder existiert
curl -s "http://127.0.0.1:5380/api/zones/list?token=$TOKEN"
# Und ob der Record wieder aufgeloest wird
dig nas.firma.local +short

Fallstrick: das unvollständige Backup direkt nach einer Änderung

Das ist der wichtigste Abschnitt dieser Anleitung, und er beschreibt einen im Test selbst reproduzierten Fehler, nicht eine theoretische Gefahr.

Der Ablauf war folgender. Direkt nach dem Anlegen der Zone firma.local über die API wurde das Verzeichnis config/zones/ angesehen. Es war leer. Ein in diesem Moment erzeugter Backup-Export enthielt nur sechs Dateien, nämlich auth.config, dns.config, allowed.config, blocked.config, blocklist.config und scopes/Default.scope. Eine Zonendatei war nicht enthalten. Erst rund 20 Sekunden später lag config/zones/firma.local.zone tatsächlich auf der Platte, und erst das danach erzeugte Backup enthielt zones/firma.local.zone, also sieben Dateien.

Die Folge war genau die, die man erwarten muss: Ein Restore aus dem ersten, unvollständigen Backup stellte die Zone nicht wieder her. Die Zonenliste blieb leer, und dig lieferte keine Antwort. Das Backup hatte HTTP 200 gemeldet, die ZIP-Datei war gültig, der Restore hatte ebenfalls HTTP 200 gemeldet. Nach allen Statuscodes sah alles in Ordnung aus. Die Zone war trotzdem weg.

Die Ursache liegt darin, dass der Server Zonenänderungen zunächst im Arbeitsspeicher hält und erst verzögert auf die Platte schreibt, während der Backup-Export den Zustand auf der Platte einpackt. Wer unmittelbar nach einer Änderung sichert, sichert deshalb möglicherweise den Stand von vorher.

Daraus folgen zwei Regeln, die Sie in jedes Backup-Skript einbauen sollten.

  • Nie unmittelbar nach einer Konfigurationsänderung sichern. Lassen Sie mindestens eine Minute Abstand, besser mehr. Ein automatisches Backup, das direkt an ein Konfigurationsskript angehängt wird, ist genau der Fall, der schiefgeht.
  • Jedes Backup nach dem Erzeugen prüfen. Ein unzip -l kostet nichts und zeigt sofort, ob zones/ enthalten ist. Ein Backup ohne Zonendateien ist für einen Server mit eigenen Zonen wertlos, egal welcher Statuscode zurückkam.
# Nach dem Export pruefen, ob ueberhaupt Zonendateien im Backup liegen
unzip -l backup.zip | grep 'zones/'
# Exitcode auswerten und im Fehlerfall abbrechen, Muster fuer ein Backup-Skript
if ! unzip -l backup.zip | grep -q 'zones/'; then
  echo "FEHLER: Backup enthaelt keine Zonendateien, nicht als gueltig ablegen"
  exit 1
fi

Der zweite Block ist das eigentliche Ergebnis dieses Tests in ausführbarer Form: eine Prüfung, die ein unvollständiges Backup erkennt, bevor Sie sich im Notfall darauf verlassen.

Sichere Netzwerkfreigabe

Ein DNS-Server gehört nicht offen ins Internet, und das ist keine Formalie. Ein rekursiver Resolver, der Anfragen aus dem Internet beantwortet, ist ein offener Resolver und damit ein Werkzeug für Angreifer. Bei einer DNS-Verstärkungsattacke schickt der Angreifer kleine Anfragen mit gefälschter Absenderadresse an Ihren Server, der schickt große Antworten an das Opfer. Ihre Leitung und Ihre IP-Adresse sind dann Teil eines DDoS-Angriffs, mit den entsprechenden Konsequenzen für Reputation und Erreichbarkeit.

Daraus folgt konkret: Geben Sie Port 53 niemals aus dem Internet frei. Binden Sie den Dienst an eine interne Adresse, beschränken Sie den Zugriff per Firewall auf Ihre internen Netze, und richten Sie in Technitium selbst ein, von welchen Netzen rekursive Abfragen erlaubt sind.

# Pruefen, auf welchen Adressen die Container-Ports tatsaechlich lauschen
ss -lntp | grep -E '5380|:53'
# Abfrage von aussen simulieren, indem die interne Adresse explizit angegeben wird
dig @192.168.10.5 -p 15353 example.com +short

Der zweite Punkt betrifft die Weboberfläche. Sie läuft auf Port 5380 unverschlüsselt über HTTP. Das bedeutet, dass Anmeldedaten und das API-Token im Klartext über das Netz gehen. Eine solche Oberfläche sollte nicht erreichbar sein, ohne dass entweder ein Reverse Proxy mit TLS davorsteht oder der Zugriff scharf beschränkt ist, etwa auf 127.0.0.1 plus einen SSH-Tunnel oder ein Management-VLAN.

Kennzeichnung: Der Betrieb hinter einem Reverse Proxy und die Nutzung von DNS-over-HTTPS oder DNS-over-TLS mit einem echten Zertifikat wurden in diesem Test nicht durchgeführt. Beides ist laut offizieller Dokumentation vorgesehen, aber die Angaben dazu sind hier dokumentiert und nicht selbst verifiziert. Wenn Sie einen Proxy davorsetzen, planen Sie ein: Die Oberfläche bleibt nur auf localhost gebunden, der Proxy terminiert TLS, und die API-Aufrufe laufen dann über HTTPS, damit das Token nicht im Klartext unterwegs ist.

Updates und Rollback-Grenzen

Der Update-Pfad wurde im Test durchgeführt. Der Image-Tag wurde von 15.5.0 auf latest geändert und der Stack neu gestartet.

# Vor jedem Update ein Backup ziehen und sofort pruefen
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/'
# Neues Image holen
docker compose pull
# Container mit dem neuen Image neu erstellen
docker compose up -d
# Startprotokoll kontrollieren
docker compose logs --tail 30 dns-server

Im Test lief der Container anschließend mit dem neuen Image, und Zone und A-Record überlebten den Image-Wechsel. Der Grund ist die Architektur: Die Daten liegen im gemounteten Verzeichnis ./config und nicht im Container. Ein Containertausch berührt sie nicht.

Warum trotzdem vor jedem Update ein geprüftes Backup? Weil ein Update mehr tun kann als nur den Code austauschen. Wenn eine neue Version das Datenformat migriert, sind Ihre Daten anschließend im neuen Format, und das gemountete Verzeichnis, das Sie gerade als Sicherheitsnetz betrachtet haben, enthält dann keinen Stand mehr, den die alte Version lesen kann.

Als allgemeine Vorsichtsregel, nicht als Testergebnis: Ein Downgrade auf eine ältere Version funktioniert nicht garantiert. Sobald ein Datenformat migriert wurde, kann die ältere Version die Dateien möglicherweise nicht mehr verarbeiten. Ein Downgrade wurde in diesem Test nicht durchgeführt. Planen Sie deshalb den Rückweg immer über ein Backup, das mit der alten Version erzeugt wurde, und nicht über einen einfachen Tagwechsel zurück. Und pinnen Sie im Produktivbetrieb eine konkrete Version statt latest, damit ein docker compose pull nicht zum unbeabsichtigten Hauptversionssprung wird.

Typische Fehler mit Diagnose und Lösung

Zu jedem Fehlerbild ein Diagnosebefehl und die Lösung.

1. Port 53 ist belegt, der Container startet nicht. Typische Meldung beim Start ist ein Fehler beim Binden der Adresse, etwa address already in use.

# Belegung von Port 53 auf UDP und TCP pruefen
ss -lnup | grep ':53'
ss -lntp | grep ':53'
# Fehlermeldung des Containers im Klartext ansehen
docker compose logs --tail 40 dns-server

Lösung: Eine der drei Optionen aus dem Abschnitt zu Port 53 wählen, also Stub-Listener abschalten, an eine bestimmte Host-IP binden, oder für einen Test einen anderen Hostport mappen.

2. Der Container startet immer wieder neu.

# Status und Neustartzaehler ansehen
docker compose ps
# Vollstaendiges Log ohne Begrenzung durchsuchen
docker compose logs dns-server | tail -60

Lösung: In fast allen Fällen steht die Ursache im Log. Häufig sind Rechteprobleme am gemounteten Verzeichnis oder eine unvollständige Umgebungsvariable. Prüfen Sie zusätzlich mit docker compose config, ob DNS_SERVER_ADMIN_PASSWORD wirklich einen Wert hat und die .env gefunden wurde.

3. Die Weboberfläche ist nicht erreichbar.

# Statuscode direkt auf dem Host abfragen
curl -s -o /dev/null -w "%{http_code}
" http://127.0.0.1:5380/
# Portmapping des Containers kontrollieren
docker compose ps

Lösung: Wenn der Aufruf auf dem Host 200 liefert, aber von außen nichts ankommt, liegt es an der Firewall des Hosts oder an einem Mapping, das nur auf 127.0.0.1 bindet. Wenn schon auf dem Host nichts antwortet, prüfen Sie, ob DNS_SERVER_WEB_SERVICE_HTTP_PORT zum rechten Teil des Portmappings passt. Zeigt das Mapping auf einen anderen Containerport als den konfigurierten, antwortet dort niemand.

4. Die eigene Zone wird nicht aufgelöst.

# Existiert die Zone ueberhaupt?
curl -s "http://127.0.0.1:5380/api/zones/list?token=$TOKEN"
# Gezielt den eigenen Server befragen, nicht den System-Resolver
dig @127.0.0.1 -p 15353 nas.firma.local +short

Lösung: Zwei Ursachen sind häufig. Erstens fragt der Client gar nicht Ihren Server, sondern noch den alten Resolver. Das erkennen Sie daran, dass die Abfrage mit explizitem @ funktioniert, ohne aber nicht. Zweitens existiert die Zone nicht oder der Record fehlt, was die Zonenliste sofort zeigt.

5. Das Backup ist unvollständig.

# Inhalt des Backups auf Zonendateien pruefen
unzip -l backup.zip | grep 'zones/'
# Gegenprobe direkt im Datenverzeichnis
ls -la config/zones

Lösung: Siehe den eigenen Abschnitt oben. Abstand zur letzten Änderung halten und jedes Backup prüfen. Ist config/zones leer, obwohl eine Zone angelegt wurde, warten Sie kurz und sehen erneut nach.

6. Aufräumen scheitert mit Permission denied.

# Besitzer der Dateien im Datenverzeichnis anzeigen
ls -la config

Lösung: Der Container läuft als root, die Dateien gehören root. Löschen über einen Wegwerf-Container mit gemountetem Pfad oder über sudo, siehe den Abschnitt zu den persistenten Daten.

Saubere Deinstallation

Warnung, bitte vorher lesen: Mit den folgenden Befehlen löschen Sie das Verzeichnis config. Darin liegen alle Zonen, alle Records, alle Benutzer und alle Einstellungen. Ohne ein Backup, das Sie per unzip -l auf das Vorhandensein von zones/ geprüft haben, sind diese Daten danach unwiederbringlich verloren. Ein Restore aus einem unvollständigen Backup bringt die Zonen nicht zurück, wie der Testfall oben zeigt. Ziehen Sie also zuerst ein Backup, prüfen Sie es, kopieren Sie es an einen anderen Ort, und erst dann löschen Sie.

Wenn Sie das gemacht haben, ist der Rest kurz.

# Schritt 1: Container stoppen, Netzwerke und anonyme Volumes entfernen
docker compose down -v --remove-orphans
# Schritt 2: Besitzverhaeltnisse pruefen, bevor geloescht wird
ls -la config
# Schritt 3: Inhalt ueber einen Wegwerf-Container loeschen, 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.5.0
docker rmi technitium/dns-server:latest

Zwei Hinweise zur Vorsicht. Die Images werden bewusst namentlich entfernt. Ein pauschales Aufräumen aller unbenutzten Images kann auf einem Host mit weiteren Containern Images löschen, die Sie noch brauchen. Und vergessen Sie nicht, die DNS-Einstellung auf Ihren Clients und im Router zurückzustellen, bevor Sie den Server abschalten. Sonst stehen die Clients ohne funktionierende Namensauflösung da, was sich dann wie ein kompletter Netzausfall anfühlt.

Passende Anleitungen auf S-EDV

Quellen

TechnitiumDNSDocker ComposeNetzwerkSelfhostingBackup