Schluss mit No-IP: DynDNS mit eigener Domain selbst bauen
Warum kostenlose DynDNS-Dienste unterwegs versagen – und wie man sie mit einer eigenen Domain, vierzig Zeilen Bash und einem systemd-Timer ersetzt.
Der Auslöser
Ein VPN-Tunnel, der plötzlich stumm bleibt. Am Handy zählt “gesendet” fröhlich hoch, “empfangen” steht auf 0 Byte. Der Server läuft, die Firewall ist offen, die Portweiterleitung stimmt.
Die Ursache lag woanders: Der DynDNS-Hostname *.ddns.net wurde vom WLAN, in dem
ich gerade hing, per DNS-Filter blockiert. Dynamic-DNS-Domains stehen bei fast jedem
Filterdienst in einer eigenen Sperrkategorie – NextDNS, AdGuard, Firmennetze,
Hotel-WLANs, Jugendschutz-Resolver. Für den Filter sieht so ein Hostname aus wie
ein Command-and-Control-Server.
Das war der Punkt, an dem ich die ganze Konstruktion über Bord geworfen habe.
Was gegen kostenlose DynDNS-Dienste spricht
Filterlisten. *.ddns.net, *.duckdns.org, *.myddns.me und Konsorten sind
pauschal gesperrt, sobald ein Netz irgendeine Form von DNS-Filterung betreibt.
Ausgerechnet unterwegs, wo man den Fernzugriff braucht.
Bestätigungspflicht. No-IP verlangt im Free-Tarif alle 30 Tage eine manuelle Bestätigung per Mail-Link. Verpasst man sie, wird der Hostname deaktiviert. Das passiert zuverlässig dann, wenn man im Urlaub ist.
Rate-Limits. Ein Updater, der alle 30 Minuten anfragt, kassiert bei manchen
Anbietern irgendwann ein 403 Updateintervall overcommited. Ab da wird nichts mehr
aktualisiert – still und ohne Alarm.
Fremdabhängigkeit. Der Dienst kann Preise ändern, Tarife streichen oder verschwinden. Meine Erreichbarkeit hängt dann an jemand anderem.
Client-Caching. Android und die WireGuard-App lösen den Endpoint nur beim Start des Tunnels auf. Wechselt die IP, schickt der Client bis zum Neustart stur an die tote Adresse. Mit kurzer TTL und eigener Kontrolle lässt sich das entschärfen.
Was man braucht
Erstaunlich wenig:
- Eine eigene Domain. Kostet ein paar Euro im Jahr.
- Einen DNS-Hoster mit API. Hetzner, Cloudflare, deSEC, netcup – die meisten bieten das.
- Einen Rechner, der immer läuft. Ein LXC-Container, ein Raspberry Pi, die Firewall. Hauptsache im Netz, dessen IP überwacht werden soll.
- curl. Das war’s an Abhängigkeiten.
Kein zusätzlicher Dienst, kein Account bei Dritten, kein Abo.
Das Konzept: ein einziger Anker
Der zentrale Kniff ist, nicht jeden Hostnamen einzeln zu aktualisieren. Stattdessen:
A dyn.example.at <dynamische IP> TTL 60 ← einziger Record, der sich ändert
CNAME vpn.example.at dyn.example.at. TTL 300
CNAME cloud.example.at dyn.example.at. TTL 300
CNAME wiki.example.at dyn.example.at. TTL 300
CNAME media.example.at dyn.example.at. TTL 300
Das Skript kennt genau eine Record-ID. Neue Dienste bekommen einfach einen weiteren CNAME – ohne dass die Automatik angefasst werden muss. Und wenn doch mal etwas schiefgeht, gibt es nur eine Stelle zum Nachsehen.
Die TTL des A-Records gehört auf 60 Sekunden. Die CNAMEs dürfen ruhig länger stehen, sie ändern sich ja nie.
Umsetzung mit der Hetzner Cloud API
Ein Hinweis vorweg: Hetzner hat die alte DNS-API unter dns.hetzner.com im Mai 2026
abgeschaltet. Alte Anleitungen und ältere ddclient-Versionen zielen ins Leere. Die
aktuelle API läuft über api.hetzner.cloud und arbeitet mit sogenannten RRSets.
Vorbereitung
In der Hetzner Console den A-Record dyn mit TTL 60 anlegen – die API kann bestehende
Records ändern, aber keine neuen erzeugen.
Dann unter Projekt → Security → API-Tokens ein Token mit Lese- und Schreibrechten erstellen. Der Wert wird nur einmal angezeigt.
Zone-ID ermitteln:
curl -s -H "Authorization: Bearer $API_TOKEN" \
"https://api.hetzner.cloud/v1/zones" | jq -r '.zones[] | "\(.name) \(.id)"'
Das Skript
#!/usr/bin/env bash
set -euo pipefail
API_TOKEN="<TOKEN>"
ZONE="<ZONE-ID>"
RRNAME="dyn"
RRTYPE="A"
CACHE="/opt/dyndns/.last_ip"
LOG="/var/log/hetzner-ddns.log"
IP=$(curl -4 -s --max-time 10 https://ifconfig.me || true)
[[ "$IP" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || {
echo "$(date '+%F %T') | keine gueltige IP" >>"$LOG"; exit 1; }
[[ -f "$CACHE" && "$(cat "$CACHE")" == "$IP" ]] && exit 0
RESP=$(curl -s -w '\n%{http_code}' -X POST \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"records\":[{\"value\":\"${IP}\",\"comment\":\"ddns\"}]}" \
"https://api.hetzner.cloud/v1/zones/${ZONE}/rrsets/${RRNAME}/${RRTYPE}/actions/set_records")
CODE=$(tail -n1 <<<"$RESP")
if [[ "$CODE" =~ ^2 ]]; then
echo "$IP" > "$CACHE"
echo "$(date '+%F %T') | OK ${RRNAME} -> ${IP}" >>"$LOG"
logger -t hetzner-ddns "A-Record ${RRNAME} auf ${IP} gesetzt"
else
echo "$(date '+%F %T') | FEHLER HTTP ${CODE}: $(head -n-1 <<<"$RESP")" >>"$LOG"
exit 1
fi
Vier Designentscheidungen, die den Unterschied machen:
- Cache-Datei: Bei unveränderter IP passiert nichts. Keine unnötigen API-Calls, keine Rate-Limit-Probleme.
- Regex-Prüfung: Fängt leere oder kaputte Antworten des IP-Dienstes ab. Sonst landet irgendwann Müll im DNS.
- HTTP-Code auswerten: Ein
curlohne Prüfung meldet Erfolg, auch wenn die API einen Fehler zurückgibt. - Stiller Exit bei Erfolg ohne Änderung: Das Log bleibt lesbar.
Rechte setzen, das Token steht im Klartext drin:
chmod 700 /opt/dyndns/hetzner-ddns.sh
Systemd-Timer statt Cron
# /etc/systemd/system/hetzner-ddns.service
[Unit]
Description=Hetzner DynDNS Update
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/dyndns/hetzner-ddns.sh
# /etc/systemd/system/hetzner-ddns.timer
[Unit]
Description=Hetzner DynDNS alle 5 Minuten
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now hetzner-ddns.timer
systemctl list-timers hetzner-ddns.timer
Warum Timer statt Cron? Persistent=true holt verpasste Läufe nach einem Neustart
nach, OnBootSec sorgt für einen Lauf direkt nach dem Hochfahren, und der Status ist
über systemctl und journalctl sauber einsehbar.
Der Teil, den die meisten Anleitungen weglassen: Überwachung
Ein Updater, der still ausfällt, ist schlimmer als keiner – man merkt es erst, wenn man von unterwegs nicht mehr reinkommt.
Ein kleines Watchdog-Skript prüft, ob die überwachten Namen tatsächlich auf die aktuelle WAN-IP zeigen, und schickt bei Abweichung eine Mail:
WAN=$(curl -4 -s https://ifconfig.me)
for h in dyn.example.at vpn.example.at; do
IP=$(dig +short "$h" @9.9.9.9 | tail -1)
[[ "$IP" == "$WAN" ]] || echo "ABWEICHUNG: $h -> $IP (erwartet $WAN)"
done
Zwei Details sind wichtig:
Externe Resolver verwenden, nicht den lokalen Pi-hole. Sonst prüft man die eigene Sicht statt der von außen.
Auch den Updater-Dienst prüfen – ein gestoppter Timer fällt sonst erst auf, wenn die IP wechselt.
Genau dieser Watchdog hat bei mir später den Ausfall eines verbliebenen No-IP-Namens gemeldet, während die eigene Domain sauber weiterlief.
Migration ohne Ausfall
Wer bestehende Dienste umzieht, sollte gestaffelt vorgehen:
dyn-Record anlegen, Updater einrichten, beide Systeme parallel laufen lassen- Ein bis zwei Tage beobachten – idealerweise über eine Zwangstrennung hinweg
- CNAMEs einzeln umhängen, nach jedem Schritt den Dienst testen
- Clients umstellen (VPN, Cloud-Apps, CalDAV, Chat)
- Erst dann die alten Updater abschalten
Schritt 4 wird gern unterschätzt. Ein Reverse Proxy arbeitet mit Host-Headern und merkt von der DNS-Umstellung gar nichts. Clients dagegen haben den alten Namen fest eingetragen – und manche, etwa DAVx5, erlauben keine nachträgliche Änderung der Basis-URL, sondern verlangen ein neues Konto.
Fallstricke aus der Praxis
Anwendungen mit Hostnamen-Prüfung. Nextcloud hat trusted_domains und
overwritehost, Paperless-ngx hat PAPERLESS_ALLOWED_HOSTS. Wer nur den DNS-Eintrag
ändert, bekommt eine Fehlerseite. Bei Nextcloud sorgt ein gesetztes overwritehost
zusätzlich dafür, dass jeder Aufruf auf den alten Namen umgeleitet wird.
Apache und VirtualHosts. Fehlt der ServerAlias für den neuen Namen, landet die
Anfrage im Default-VHost – und man sieht die Ubuntu-Standardseite statt der Anwendung.
Kein AAAA-Record anlegen, solange kein IPv6-Dienst dahinter lauscht. Clients mit IPv6 bevorzugen sonst die AAAA und laufen ins Leere.
Subnetz-Kollisionen. Wer sein Heimnetz auf 192.168.1.0/24 betreibt, kommt per
VPN aus jedem Hotel-WLAN mit demselben Bereich nicht an die eigenen Geräte – die
lokale Route gewinnt immer. Ein unüblicher Bereich wie 192.168.47.0/24 erspart viel
Fehlersuche.
Erst prüfen, dann konfigurieren. Ich habe einmal eine halbe Stunde an der falschen
Instanz geschraubt, weil ein zweiter Container dieselbe Rolle hatte und ich die IP
nicht gegengeprüft hatte. Ein arp-Eintrag oder ein Blick in die DHCP-Leases hätte
das sofort geklärt.
Alternativen zu Hetzner
Das Prinzip funktioniert mit jedem Hoster, der eine Schreib-API bietet:
Cloudflare – kostenlos, gute API, ddclient unterstützt es nativ. Wichtig: Der Proxy-Status muss auf “DNS only” stehen (graue Wolke), sonst funktioniert kein UDP und damit kein WireGuard.
deSEC.io – gemeinnützig, Open Source, DNSSEC standardmäßig. Man kann eine
dedyn.io-Domain nehmen oder eine Subdomain der eigenen Zone dorthin delegieren.
ddclient – wer nicht selbst skripten will: unterstützt Cloudflare, deSEC, Hetzner (alte API!), Gandi, Namecheap und viele mehr. Vor dem Einsatz prüfen, ob der Anbieter noch die API-Version verwendet, die das installierte ddclient kennt.
Fazit
Ein DynDNS-Dienst ist letztlich nichts weiter als “Domain + API + Updater”. Zwei davon hat man meist schon, das dritte sind vierzig Zeilen Bash.
Was man dafür bekommt: keine Filterprobleme unterwegs, keine Bestätigungsfristen, kontrollierbare TTL, keine Rate-Limits – und eine Stelle weniger, an der jemand anders über die eigene Erreichbarkeit entscheidet.
Der Aufwand liegt bei einem gemütlichen Nachmittag. Die Migration bestehender Dienste dauert länger als der Aufbau selbst, aber sie lässt sich ohne Ausfall gestaffelt erledigen.