Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Hetzner DynDNS Bridge: Hetzner Console + DNS Console API Support

profile picture
Author
woehrl
Published
2026-02-03
Time to read
8 minutes reading time

About the author- S/W Architect, Full-Stack Senior Developer

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 curl and SQLite3 extensions (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 ServerName with 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      Optional

Example 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
done

Step 2 - Create your config

sudo cp hetzner_dyndns.config.php.dist hetzner_dyndns.config.php

Edit 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:
  • Hetzner Console: create a API token in the Hetzner Console and paste it into console_token.
  • DNS Console (legacy): create a DNS token in DNS Console and paste it into dns_token (only if you still host zones there).
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.php out of public Git and file listings.
  • Ensure .htaccess rewrites 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 to update if empty). Most DynDNS clients send any username plus the password, and you can also curl it:
    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) or nochg <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/update URL with the password you set. IPv6 works too when the client sends myipv6.
  • 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 .htaccess not 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_order and 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/update or /v3/update) are rewritten to hetzner_dyndns.php.
    • The script authenticates with your shared username/password, parses the hostname and optional realm override, caches known records in SQLite, and returns good immediately when nothing changed.

  • Configuration deep dive
    • Shared settings: auth_user, auth_password, auth_realm, history_db, and optional debug/debug_log.
    • Notifications: enable notifications.enabled, pick php or smtp, set recipients, and decide if you want messages on success, failure, or both.
    • Realms: each realm holds ttl, api_order, zone_name, console_token, and dns_token. Override zone_name if your DynDNS endpoint lives on a subdomain but you update the parent zone.

  • Hetzner Console API flow (current API)
    1. Resolve the zone via /zones?name=<zone> using the Hetzner Console API token.
    2. Fetch RRsets via /zones/{id}/rrsets (A and AAAA).
    3. Match the RRset name to your host (handles @ at the apex).
    4. Call set_records to replace the records with the new IPs. Successful calls return an action; failures log not_found or permission errors.

  • DNS Console API flow (legacy)
    1. Look up the zone ID with /zones?name=<zone> using dns_token.
    2. Find A/AAAA record IDs, cache them in SQLite, and update via PUT /records/{id}.
    3. Failures stay marked needs_sync for cron retries.

  • Transitioning between APIs
    • Use ['console', 'dns'] while migrating. If Hetzner Console says not_found, the script falls back to DNS Console.
    • When a zone is fully on Console, drop dns_token or switch to ['console'] to avoid legacy calls and to get ahead of the DNS Console shutdown timeline.
    • Run php hetzner_dyndns.php --cron after config changes to flush pending work and confirm both APIs behave.

  • Multiple zones and realms
    • Create one realm per zone or per subdomain. Example: a gjsi.de realm and a ddns.gjsi.de realm 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.php can print cached IPv4/IPv6 per realm without hitting the HTTP endpoint.

  • Notifications and observability
    • Email notifications report successes and failures using PHP mail() or SMTP.
    • debug and debug_log capture request and response pairs for both APIs plus notification status.
    • Cron summaries print totals, successes, failures, and which API handled each host.

  • Security and deployment notes
    • .htaccess blocks 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.

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue
Try Hetzner Cloud

Get €20/$20 free credit!

Valid until: 31 December 2026 Valid for: 3 months and only for new customers
Get started
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more