Introduction
Need your IPv4/IPv6 address to follow you around the internet without babysitting your DNS? This tutorial walks you through a small PHP script that pretends to be a DynDNS server, speaks to both Hetzner Console API and the legacy DNS Console API, and plays nice with routers like a Fritz!Box. The first half is a hand-holding setup guide. The second half is the nerd zone with all the gory details. New DNS zones can no longer be created in DNS Console (as of 10 Nov 2025); plan to migrate every zone to Hetzner Console and only keep DNS Console enabled while you finish that move.
Prerequisites
- A Hetzner account with at least one DNS zone (Hetzner Console or DNS Console during migration).
- PHP with the
curlandSQLite3extensions (typical web hosting or a small VM works fine). - Somewhere to drop a PHP file and run cron every few minutes.
- A client that can call a DynDNS-style URL (router, NAS, or a simple curl command).
Step 0 - Minimalistic example setup on Debian/Ubuntu
If you start from a fresh minimal Debian/Ubuntu host, this gets you to a working test endpoint:
- Install prerequisites and add directory for scripts
sudo apt update sudo apt install -y apache2 libapache2-mod-php php-cli php-curl php-sqlite3 sudo a2enmod rewrite sudo mkdir -p /var/www/hetzner-ddns sudo chown -R www-data:www-data /var/www/hetzner-ddns
-
Create a minimal Apache site
Replace the value of
ServerNamewith your DynDNS endpoint. Use HTTPS in production (Let's Encrypt works fine).cat <<'EOF' | sudo tee /etc/apache2/sites-available/hetzner-ddns.conf <VirtualHost *:80> ServerName ddns.example.com DocumentRoot /var/www/hetzner-ddns <Directory /var/www/hetzner-ddns> AllowOverride All Require all granted </Directory> </VirtualHost> EOF
- Enable site
sudo a2ensite hetzner-ddns sudo apachectl configtest sudo systemctl reload apache2
Step 1 - Grab the files
Use the files bundled with this tutorial in tutorials/hetzner-ddns-bridge/scripts:
Mirrored from https://github.com/woehrl/hetzner-dyndns, commit
11582e6
Copy the files to your web space or small VM. If you use the minimalistic example setup above, you need to save the files in /var/www/hetzner-ddns.
You need at least:
/var/www/hetzner-ddns
├─ hetzner_dyndns.php
├─ hetzner_dyndns.config.php.dist
├─ .htaccess The one provided on GitHub
└─ hetzner_dyndns_listhosts.php OptionalExample commands:
export path="https://raw.githubusercontent.com/hetzneronline/community-content/refs/heads/master/tutorials/hetzner-ddns-bridge/scripts"
cd /var/www/hetzner-ddns
# Run this in the terminal to set the filenames
files=(
hetzner_dyndns.php
hetzner_dyndns.config.php.dist
.htaccess
hetzner_dyndns_listhosts.php
)
# Run this in the terminal to copy the files
for f in "${files[@]}"; do
curl "$path/$f" | sudo tee "$f" >/dev/null
doneStep 2 - Create your config
sudo cp hetzner_dyndns.config.php.dist hetzner_dyndns.config.phpEdit hetzner_dyndns.config.php:
| Description | |
|---|---|
| auth_user | Optional: Username for HTTP Basic Auth. If empty, it defaults to update. |
| auth_password | Set a strong shared password. Your router will use this. |
| api_order | Decide which API to try first: set to ['console', 'dns'] for a smooth migration or ['console'] once every zone is on the new API. |
| console_token dns_token |
Add tokens:
|
| zone_name | If you want to update the IP address of a subdomain (e.g. sub.example.com), specify the parent domain (e.g. example.com) here. |
| auth_realm | Optionally: Pick a label (anything friendly like "dynbridge"). |
| history_db | Optionally: Point to a writable path (for example DIR . '/hetzner_dyndns.sqlite3'). |
| TTL | Optionally: Adjust TTL per realm if you want faster or slower propagation. |
Step 3 - Put it on the internet (safely)
- Keep
hetzner_dyndns.config.phpout of public Git and file listings. - Ensure
.htaccessrewrites DynDNS endpoints to the script so old clients work:RewriteEngine On RewriteRule ^(nic/update|v3/update)$ hetzner_dyndns.php [L,QSA] - If your host needs it, set the PHP user to be able to write the SQLite file and debug log.
Step 4 - Test an update
- Build the test URL (substitute your domain and desired host):
https://your-ddns-host.example.com/nic/update?hostname=myhost.example.com&myip=203.0.113.10 - Use HTTP Basic Auth with your
auth_user/auth_password(user defaults toupdateif empty). Most DynDNS clients send any username plus the password, and you can alsocurlit:curl -u update:yourSecret \ "https://your-ddns-host.example.com/nic/update?hostname=myhost.example.com&myip=$(curl -s ifconfig.me)" - A happy response is
good <ip>(already updated) ornochg <ip>(nothing to do). Anything else means check the debug log.
Step 5 - Make it automatic
- Add cron every 5 minutes (tweak as you like):
*/5 * * * * php /path/to/hetzner_dyndns.php --cron --realm=default - Point your router or NAS DynDNS profile to the same
nic/updateURL with the password you set. IPv6 works too when the client sendsmyipv6. - Legacy clients can still send
X-Authentication: <password>or?p=<password>(password-only), but Basic Auth is recommended.
Step 6 - Quick troubleshooting checklist
- 401 or prompt for auth: password mismatch or
.htaccessnot applied. not_found: wrong zone or hostname, or missing token permissions.- SQLite write errors: fix file permissions or move the DB to a writable folder.
- Nothing changes: check
api_orderand make sure you set the right token for the realm.
Nerd corner (how it actually works)
- Architecture at a glance
- One PHP file, one config file, one SQLite database. Incoming DynDNS calls (
/nic/updateor/v3/update) are rewritten tohetzner_dyndns.php. - The script authenticates with your shared username/password, parses the hostname and optional
realmoverride, caches known records in SQLite, and returnsgoodimmediately when nothing changed.
- One PHP file, one config file, one SQLite database. Incoming DynDNS calls (
- Configuration deep dive
- Shared settings:
auth_user,auth_password,auth_realm,history_db, and optionaldebug/debug_log. - Notifications: enable
notifications.enabled, pickphporsmtp, set recipients, and decide if you want messages on success, failure, or both. - Realms: each realm holds
ttl,api_order,zone_name,console_token, anddns_token. Overridezone_nameif your DynDNS endpoint lives on a subdomain but you update the parent zone.
- Shared settings:
- Hetzner Console API flow (current API)
- Resolve the zone via
/zones?name=<zone>using the Hetzner Console API token. - Fetch RRsets via
/zones/{id}/rrsets(A and AAAA). - Match the RRset name to your host (handles
@at the apex). - Call
set_recordsto replace the records with the new IPs. Successful calls return an action; failures lognot_foundor permission errors.
- Resolve the zone via
- DNS Console API flow (legacy)
- Look up the zone ID with
/zones?name=<zone>usingdns_token. - Find A/AAAA record IDs, cache them in SQLite, and update via
PUT /records/{id}. - Failures stay marked
needs_syncfor cron retries.
- Look up the zone ID with
- Transitioning between APIs
- Use
['console', 'dns']while migrating. If Hetzner Console saysnot_found, the script falls back to DNS Console. - When a zone is fully on Console, drop
dns_tokenor switch to['console']to avoid legacy calls and to get ahead of the DNS Console shutdown timeline. - Run
php hetzner_dyndns.php --cronafter config changes to flush pending work and confirm both APIs behave.
- Use
- Multiple zones and realms
- Create one realm per zone or per subdomain. Example: a
gjsi.derealm and addns.gjsi.derealm with different tokens and TTLs. - Use
realm=<name>in the DynDNS query string if you host multiple domains with the same endpoint. - The optional CLI helper
hetzner_dyndns_listhosts.phpcan print cached IPv4/IPv6 per realm without hitting the HTTP endpoint.
- Create one realm per zone or per subdomain. Example: a
- Notifications and observability
- Email notifications report successes and failures using PHP
mail()or SMTP. debuganddebug_logcapture request and response pairs for both APIs plus notification status.- Cron summaries print totals, successes, failures, and which API handled each host.
- Email notifications report successes and failures using PHP
- Security and deployment notes
.htaccessblocks direct hits to the PHP and SQLite files and only exposes the DynDNS endpoints.- Keep tokens scoped to DNS and rotate them periodically.
- Ensure the SQLite DB and debug log are writable by the PHP user; keep them out of public web roots when possible.
- Use HTTPS on your DynDNS endpoint. Plain HTTP would leak your shared password.
Conclusion
The bridge gives you a friendly DynDNS endpoint that survives Hetzner's API migration. Start with the quickstart steps to get updates flowing, then dip into the nerd corner when you want to tune realms, notifications, or the Hetzner Console / DNS Console split.