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
| Piece | What it is | What it does |
|---|---|---|
| Desktop Client | Electron app in your system tray | Watches your public IP and calls the API when it changes |
| Node.js API | Express server on your VPS | Validates your token, tells PowerDNS to update the record, logs history |
| PowerDNS | Authoritative DNS server on your VPS | Answers DNS queries from the internet with the current IP |
| React Dashboard | Web UI on your VPS | Shows 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
.comaddresses - The
.compost office knows which registrar handlesdevops-monk.com - The registrar (Porkbun) handles
devops-monk.com— but you tell it: “For anything underddns.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:
- You register
homelabon the dashboard — the API generates a random token and stores its bcrypt hash in PostgreSQL (never the raw token). - You paste the raw token into the desktop client. The client stores it in local config.
- On every update call the client sends the token in
Authorization: Bearer TOKEN. - The API looks up the domain record, runs
bcrypt.compare(token, storedHash), and rejects the request if it does not match. - 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.
| Feature | DuckDNS | No-IP Free | DevOps Monk DDNS |
|---|---|---|---|
| Price | Free | Free | Free |
| Domain limit | 5 subdomains | 1 hostname | 5 subdomains |
| Monthly confirmation | No | Required | No |
| IP change history | No | No | Yes |
| Desktop app | No | Yes | Yes |
| Open source | Yes | No | Yes |
| Self-hosted option | No | No | Yes |
| IPv6 support | Yes | Yes | Yes |
| Custom DNS zone | No | No | Yes |
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?
- Go to ddns.devops-monk.com and create a free account
- Register a subdomain — for example
homelab.ddns.devops-monk.com - Download the desktop client for your OS, or drop a one-line cron job on headless servers
- 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.