Build Your Own DDNS Platform

If you run a home server — a Raspberry Pi, a NAS, a Kubernetes cluster in your garage — you have probably hit the same annoying wall: your internet provider gives you a different public IP address every few days, and suddenly nobody can reach your server anymore. This post explains how I solved that problem by building ddns.devops-monk.com, a fully self-hosted Dynamic DNS platform. I will walk through the idea from scratch, explain every moving part in plain English, and include full architecture diagrams for those who want the deep technical picture.


Start Here: What Problem Are We Solving?

Think of a domain name like a contact name in your phone. When someone wants to call you, they look up your name and your phone shows the number. DNS does the same thing for servers — someone types homelab.ddns.devops-monk.com and DNS tells their computer which IP address to connect to.

The problem is that most home internet connections have a dynamic IP address — your router gets a new public IP every time it reconnects or every few days. It is like if your phone number changed randomly and nobody updated their contacts.

Dynamic DNS (DDNS) is the solution: a small program runs on your machine, watches for IP changes, and automatically updates the DNS record the moment your IP rotates. Everyone who visits your hostname always gets the current address.

flowchart LR
    A[Your Home Server] -->|IP changes| B[DDNS Client]
    B -->|sends new IP| C[DDNS API]
    C -->|updates DNS record| D[Your Hostname]
    D -->|visitors connect| A

The Big Picture: All Four Pieces

The platform is made of four pieces that work together. Here is how they relate before we go into each one:

flowchart TD
    subgraph Machine[Your Machine]
        CLIENT[Desktop Client]
        CRON[Cron Job]
    end
    subgraph VPS[Your VPS]
        DASHBOARD[React Dashboard]
        API[Node.js API]
        PDNS[PowerDNS]
        PGDB[(PostgreSQL)]
        MYDB[(MySQL)]
    end
    subgraph Net[Internet]
        RESOLVER[DNS Resolvers]
        WEBHOOK[Webhook Targets]
    end
    CLIENT -->|PUT /update| API
    CRON -->|PUT /update| API
    DASHBOARD -->|REST API| API
    API --> PGDB
    API -->|PATCH rrsets| PDNS
    API --> WEBHOOK
    PDNS --> MYDB
    RESOLVER -->|DNS query port 53| PDNS
PieceWhat it isWhat it does
Desktop ClientElectron app in your system trayWatches your public IP and calls the API when it changes
Node.js APIExpress server on your VPSValidates your token, tells PowerDNS to update the record, logs history
PowerDNSAuthoritative DNS server on your VPSAnswers DNS queries from the internet with the current IP
React DashboardWeb UI on your VPSShows all your domains, IP history charts, and uptime

Understanding DNS Delegation (The Part Everyone Gets Wrong)

Before diving deeper, we need to understand one concept: delegation. This is how the internet knows to ask your PowerDNS server about ddns.devops-monk.com instead of some random server.

A postal analogy

Imagine the global DNS system as a network of post offices:

  • The Root post office knows which country handles .com addresses
  • The .com post office knows which registrar handles devops-monk.com
  • The registrar (Porkbun) handles devops-monk.com — but you tell it: “For anything under ddns.devops-monk.com, ask my VPS”
  • Your VPS (PowerDNS) then has the final answer

This hand-off is called delegation, and it is set up with two DNS records at your registrar:

ddns   NS   ns1.devops-monk.com      ← "for ddns.*, ask this nameserver"
ns1    A    YOUR_VPS_IP              ← "here is where that nameserver lives" (glue record)

Once these two records exist, the entire ddns.devops-monk.com zone is under your control.

flowchart TD
    ROOT[Root DNS]
    COM[dot-com TLD]
    REG[Registrar - devops-monk.com]
    VPS[Your VPS - PowerDNS]
    RECORD[homelab A record in MySQL]

    ROOT -->|delegates dot-com| COM
    COM -->|delegates devops-monk.com| REG
    REG -->|NS record delegates ddns zone| VPS
    VPS -->|SQL lookup| RECORD

How a DNS Lookup Works Step by Step

Here is the full sequence when someone types homelab.ddns.devops-monk.com in their browser for the first time. Each arrow is a real network request:

sequenceDiagram
    participant Browser
    participant Resolver as ISP Resolver
    participant Root as Root NS
    participant TLD as TLD NS
    participant Reg as Registrar NS
    participant PDNS as PowerDNS

    Browser->>Resolver: resolve homelab.ddns.devops-monk.com
    Resolver->>Root: who handles dot-com
    Root-->>Resolver: ask TLD nameservers
    Resolver->>TLD: who handles devops-monk.com
    TLD-->>Resolver: ask registrar nameservers
    Resolver->>Reg: who handles ddns.devops-monk.com
    Reg-->>Resolver: delegated to ns1.devops-monk.com
    Resolver->>PDNS: resolve homelab.ddns.devops-monk.com
    PDNS-->>Resolver: A 203.0.113.42 TTL 60s
    Resolver-->>Browser: 203.0.113.42
    Note over Resolver,PDNS: Cached for 60s then re-queried directly from PowerDNS

The TTL of 60 seconds is the key setting. It tells resolvers: “do not cache this for long, the IP might change soon.” That is how users pick up your new IP within a minute of it rotating.


How the IP Update Works

This is what happens on your machine every five minutes. The desktop client runs silently in the background and only does real work when your IP actually changes:

sequenceDiagram
    participant Client as Desktop Client
    participant Ipify as ipify.org
    participant API as Node.js API
    participant PG as PostgreSQL
    participant PDNS as PowerDNS
    participant WH as Webhook

    loop Every 5 minutes
        Client->>Ipify: get public IP
        Ipify-->>Client: 203.0.113.42
        Client->>Client: compare with last known IP
        alt IP not changed
            Client->>Client: skip until next poll
        else IP changed
            Client->>API: PUT /api/domains/homelab/update
            API->>API: verify Bearer token via bcrypt
            API->>PDNS: PATCH zone A record TTL 60
            PDNS-->>API: 204 success
            API->>PG: log ip_history
            API-->>Client: record updated
            API->>WH: IP changed notification
        end
    end

Notice the alt branch — on most polls nothing happens at all. The client only contacts the API when the IP is different from the last time it checked. This keeps the system quiet and efficient.


Component Deep Dive

Now that you have seen the flows, here is a closer look at how each component is built internally:

flowchart TD
    subgraph ClientBox[Desktop Client - Electron]
        TRAY[System Tray Icon]
        POLL[5-minute poll loop]
        CFG[Local config - token and subdomain]
        TRAY --> POLL
        POLL --> CFG
    end
    subgraph APIBox[Node.js API - Express]
        ROUTER[PUT /update route]
        AUTH[token auth middleware]
        PDNS_C[PowerDNS REST client]
        LOG[IP history logger]
        WH_S[Webhook dispatcher]
        ROUTER --> AUTH
        AUTH --> PDNS_C
        AUTH --> LOG
        AUTH --> WH_S
    end
    subgraph PDNSBox[PowerDNS]
        REST[REST API port 8081 localhost only]
        GM[gmysql backend]
        DNS53[DNS port 53 public]
        REST --> GM
        DNS53 --> GM
    end
    subgraph DataBox[Data Stores]
        PG2[(PostgreSQL)]
        MY2[(MySQL)]
    end
    ClientBox -->|HTTPS| APIBox
    APIBox -->|localhost HTTP| PDNSBox
    APIBox --> PG2
    PDNSBox --> MY2

Key security note: PowerDNS port 8081 is bound to 127.0.0.1 only. The Node.js API is the only process that can reach it. The internet only ever sees port 53 (DNS) and port 443 (your API/dashboard over HTTPS).


The Token Authentication Model

Every subdomain gets its own API token. Here is the full lifecycle:

  1. You register homelab on the dashboard — the API generates a random token and stores its bcrypt hash in PostgreSQL (never the raw token).
  2. You paste the raw token into the desktop client. The client stores it in local config.
  3. On every update call the client sends the token in Authorization: Bearer TOKEN.
  4. The API looks up the domain record, runs bcrypt.compare(token, storedHash), and rejects the request if it does not match.
  5. If you ever need to revoke access, regenerate the token from the dashboard — the old token immediately stops working.
sequenceDiagram
    participant User
    participant Dashboard as React Dashboard
    participant API as Node.js API
    participant PG as PostgreSQL
    participant Client as Desktop Client

    User->>Dashboard: register subdomain homelab
    Dashboard->>API: POST /api/domains
    API->>API: generate random token
    API->>API: bcrypt hash the token
    API->>PG: store domain and hashed token
    API-->>Dashboard: return raw token shown once
    Dashboard-->>User: copy and save this token
    User->>Client: paste token into client config
    Note over Client,PG: client sends token on every update call

Setting Up the Platform

What you need

  • A VPS with a static IP address (e.g. DigitalOcean, Hetzner, Linode)
  • A domain name where you can edit NS records at the registrar
  • Docker installed on the VPS

Step 1 — Configure DNS delegation at your registrar

Log into your registrar (e.g. Porkbun) and add two records:

ddns   NS    ns1.yourdomain.com
ns1    A     YOUR_VPS_IP

Step 2 — Deploy the platform

git clone https://github.com/devops-monk/home_static_ip.git
cd home_static_ip
cp .env.example .env
# Edit .env: set DATABASE_URL, PDNS_API_KEY, JWT_SECRET
docker compose up -d

Step 3 — Configure PowerDNS

Create the zone in MySQL so PowerDNS knows it is authoritative:

INSERT INTO domains (name, type) VALUES ('ddns.yourdomain.com', 'NATIVE');

Then add the SOA, NS, and apex A records (see the DNS setup guide).

Step 4 — Create your first subdomain

Go to your dashboard, sign up, and register a subdomain. Copy the API token.

Step 5 — Install the client

Download the desktop client for your OS from the Downloads page, paste in your token and subdomain, and click Save. Your DNS record updates immediately and stays current automatically.

No desktop? Use a cron job instead:

*/5 * * * * curl -s -X PUT \
  -H "Authorization: Bearer YOUR_TOKEN" \
  https://ddns.yourdomain.com/api/domains/homelab/update

How the Dashboard Helps You

The React dashboard gives you visibility into everything happening with your domains:

  • Domain list — current IP, last update time, uptime percentage
  • IP history chart — see every IP change over the last hour, 3 hours, 24 hours, or 7 days
  • Status page — live health checks confirming the API and DNS are responding correctly
  • Downloads — one-click binaries for macOS (Apple Silicon and Intel), Linux (deb and AppImage), and Windows

How We Compare

ddns.devops-monk.com is completely free to use. Sign up, register a subdomain, and start using it — no credit card, no subscription, no monthly confirmation emails.

FeatureDuckDNSNo-IP FreeDevOps Monk DDNS
PriceFreeFreeFree
Domain limit5 subdomains1 hostname5 subdomains
Monthly confirmationNoRequiredNo
IP change historyNoNoYes
Desktop appNoYesYes
Open sourceYesNoYes
Self-hosted optionNoNoYes
IPv6 supportYesYesYes
Custom DNS zoneNoNoYes

The biggest practical difference with No-IP: they silently delete your hostname if you do not log in and confirm it every 30 days. Discovering that at 2am when your homelab goes dark is not fun. DuckDNS is solid but offers no IP history, no desktop client, and no custom DNS zone — you are stuck on duckdns.org. With DevOps Monk DDNS you get everything, completely free.


Try It Now — It’s Free

Ready to stop worrying about your dynamic IP?

  1. Go to ddns.devops-monk.com and create a free account
  2. Register a subdomain — for example homelab.ddns.devops-monk.com
  3. Download the desktop client for your OS, or drop a one-line cron job on headless servers
  4. Your hostname stays reachable 24/7, no matter how often your ISP rotates your IP

No credit card. No expiry. No monthly confirmation emails. Just a hostname that works.


What Is Coming Next

  • IPv6 / AAAA records — for networks that assign dynamic IPv6 prefixes
  • Multi-zone support — bring your own subdomain zone instead of sharing ddns.devops-monk.com
  • Mobile client — update from an iOS or Android hotspot
  • Terraform provider — declare DDNS domains as infrastructure code

The platform is live at ddns.devops-monk.com. Sign up, try it out, and if you self-host it feel free to open issues or PRs — all contributions are welcome.