# ddns-updater: a self-hosted DDNS client for Cloudflare, Namecheap and 50 other providers

> qdm12/ddns-updater keeps A and AAAA records pointed at your current IP, with a Web UI, a JSON config file and a 12MB Docker image. It is a good fit for homelabs and small self-hosted setups, less so if you need a hosted service or instant failover.

**qdm12/ddns-updater** — Container to update DNS records periodically with WebUI for many DNS providers

- Repository: https://github.com/qdm12/ddns-updater
- Website: https://hub.docker.com/r/qmcgaw/ddns-updater/
- Stars: 3,229 · Forks: 284
- Language: Go
- License: MIT
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/qdm12-ddns-updater

## What ddns-updater replaces in a homelab

A residential or small-office connection usually gets an address that changes without warning. Anything addressed by hostname, a VPN endpoint, a reverse proxy, a game server, breaks the moment the address moves. The usual fix is a client that polls its own public IP and calls the DNS provider's API when the value differs from what the record holds.

Most providers ship such a client, but each one is its own binary, its own config format and its own idea of how often to check. Run three domains at three registrars and you have three of them. ddns-updater collapses that into one process: one JSON file listing every record, one scheduler, one Web UI, one set of logs. The README describes it as a program to keep DNS A and AAAA records updated for multiple DNS providers, and the provider list runs from Aliyun and Cloudflare through Namecheap, NoIP, OVH and Route53 to smaller European registrars like Netcup, Infomaniak and Domeneshop.

The audience is narrow and clear. If you already run Docker or Kubernetes on a home server, a NAS or a small VPS, this is a drop-in container. If you have no always-on machine, the project has nothing for you: it is a client you host, not a service you sign up for.

## The update loop, the IP fetchers and the JSON state file

The mechanism is a periodic loop, not an event listener. The docker-compose file sets PERIOD=5m, so every five minutes the program determines the current public IP, compares it against the stored value for each configured record, and calls the provider API only when something changed. UPDATE_COOLDOWN_PERIOD=5m is a second guard that stops the same record from being rewritten repeatedly in a short window.

Determining the public IP is itself a layered problem, and the environment variables show the project treats it that way. PUBLICIP_FETCHERS=all, PUBLICIP_HTTP_PROVIDERS=all, PUBLICIPV4_HTTP_PROVIDERS=all, PUBLICIPV6_HTTP_PROVIDERS=all and PUBLICIP_DNS_PROVIDERS=all let the program try HTTP endpoints and DNS-based lookups, with PUBLICIP_DNS_TIMEOUT=3s and HTTP_TIMEOUT=10s bounding each attempt. Using all fetchers is a resilience choice with a cost: more outbound requests, and a longer worst case before a failure is declared.

State lives in a JSON file, updates.json, which the README says stores old IP addresses with change times for each record. That file is why the ./data volume mount matters. Delete it and the program loses its notion of the previous address; on the next cycle it may see a mismatch and write records that were already correct. Back it up along with config.json.

Notifications go out through Shoutrrr via the SHOUTRRR_ADDRESSES variable, so a failed update can reach a chat service or mail without a separate monitoring stack.

## Installing with Docker Compose and adding a Cloudflare record

The repository ships a docker-compose.yml, and the README's container section starts by telling you to create a data directory owned by user id 1000. That ownership detail is not decoration: the container runs as that user, and a root-owned directory will leave you with a config file the program cannot read.

The compose file below is the project's own, trimmed to the parts you need for a first run. Note the volume line: ./data on the host maps to /updater/data inside the container, which is where config.json and updates.json are expected to live.

```yaml
services:
  ddns-updater:
    image: ghcr.io/qdm12/ddns-updater
    container_name: ddns-updater
    ports:
      - 8000:8000/tcp
    volumes:
      - ./data:/updater/data
    environment:
      - PERIOD=5m
      - LISTENING_ADDRESS=:8000
      - LOG_LEVEL=info
    restart: always
```

With the stack up, the Web UI is reachable on port 8000 of the host. Now create data/config.json. The README's binary setup section gives a Namecheap example; the same shape applies to other providers, with provider-specific credential keys that the configuration section lists per provider.

```json
{
    "settings": [
        {
            "provider": "namecheap",
            "domain": "sub.example.com",
            "password": "e5322165c1d74692bfa6d807100c0310"
        }
    ]
}
```

Restart the container after writing the file. What you should see in the logs is the program reading the settings, resolving the public IP through its fetchers, and then either reporting no change or an update for sub.example.com. The Web UI shows each record with its last update time, which is the fastest way to confirm the credential is accepted rather than silently rejected.

## Running it as a plain binary on Windows, macOS or Linux

Docker is the common path, but the project also publishes zero-dependency binaries for Linux, Windows and macOS on the releases page, and the README notes an AUR package, ddns-updater, for Arch users. The binary route has a different layout from the container: you place the executable somewhere, create a data directory next to it, and put config.json inside that directory.

```bash
chmod +x ddns-updater
mkdir data
./ddns-updater
```

On Windows the executable is ddns-updater.exe and the README says it can be launched by double-clicking. If you prefer to build from source, the README gives the Go install path directly:

```bash
go install github.com/qdm12/ddns-updater/cmd/ddns-updater@latest
```

Behaviour is configurable through environment variables or flags, with a one-to-one mapping: each variable has a flag with the same name in lowercase and underscores replaced by dashes, so LOG_LEVEL becomes --log-level. That symmetry is convenient for anyone moving between a systemd unit and a container, and it means the compose file doubles as documentation for the binary.

## Where ddns-updater is the wrong tool

The polling interval is the first limitation. With PERIOD=5m, a changed address can be stale for up to five minutes, and the cooldown can extend the gap for a record that flaps. For a home VPN that is fine. For anything that must stay reachable through an address change, five minutes of downtime is a real outage, and a client that reacts to interface events rather than polling would be the better design.

The provider list is long, but it is a list of adapters, and each adapter has its own credential model. The README points to a configuration section for the per-provider fields; it does not promise that every provider supports every record type, and the feature list itself says A records while the project description says A and AAAA. Before committing, read the entry for your provider rather than assuming parity with the others.

Operationally, the failure mode to watch is silent. If a provider changes its API or a token expires, the loop keeps running and the record keeps pointing at the old address. The Docker healthcheck described in the README verifies DNS resolution of your domains, which catches that case only if you are running the container and something is watching the health status. Without SHOUTRRR_ADDRESSES configured, nobody is told.

Finally, this is not a DNS server, a certificate manager or a reverse proxy. It edits records at a provider that already hosts your zone. If you want to run authoritative DNS yourself, you are looking at a different category of software.

## ddclient and DuckDNS: what changes

The nearest comparison is ddclient, the long-standing Perl client that ships in most distributions. Both poll, both write A records, both support many providers. The difference is packaging and surface area. ddclient is a system package with a Perl configuration file and no web interface; ddns-updater is a Go binary or a 12MB Scratch-based container with a JSON config and a Web UI on port 8000. If your environment is already containerized and you would rather not install Perl on the host, ddns-updater fits more naturally. If you want a package your distribution maintains and patches, ddclient is the lower-friction choice.

DuckDNS comes up often as well, and it is a different kind of thing: a free dynamic DNS service with its own domains, plus a simple update endpoint. ddns-updater can talk to DuckDNS as one of its providers. The trade-off is domain ownership. With DuckDNS you get a name under their zone and no registrar account to manage; with ddns-updater plus Cloudflare or Namecheap you keep your own domain and pay for it. Choosing between them is really a question of whether you want a free subdomain or control of the zone.

## Maintenance, upgrades and the MIT licence

The repository is not archived and the last push was on 2026-09-16, with v2.10.0 released on 2026-05-01. The gap between v2.9.0 in December 2024 and v2.9.1 in May 2026 is worth noting if you plan around release cadence: this is a project that moves in bursts rather than on a schedule, and the README's versioned documentation table exists precisely because the config format has changed across v2.5 through v2.8.

That versioning has a practical consequence for upgrades. The README and the docs directory are tagged to match the program version, so when you pull a new image you should read the README at that tag rather than the master copy, and check whether keys in your config.json still apply. The compose file's environment variables are the other place to look, since defaults such as PERIOD and the PUBLICIP_* fetcher settings are read at startup.

The licence is MIT, which permits commercial use, modification and redistribution provided the copyright notice and permission notice are included. That is a permissive arrangement, but it says nothing about the DNS providers themselves: their API terms, rate limits and acceptable-use rules are separate agreements between you and each provider, and the MIT grant does not cover them. Nothing here is legal advice; read the LICENSE file and your provider's terms if the deployment is commercial.

## Conclusion

Adopt ddns-updater if you run a homelab or small self-hosted service and want one process covering several domains and providers, with a JSON config you can back up and a Web UI on port 8000 for checking last update times. Do not adopt it if you want a hosted DDNS service with no container to run, or if you need sub-second failover when your IP changes. Verify first that your provider appears in the supported list and that the credential fields for it match what the configuration section documents, then confirm the container can write to the mounted ./data directory, because that is where updates.json persistence lives.

## FAQ

### How do I set up DDNS Updater?

Create a data directory, write your records into data/config.json, and start the program either as the binary or as the container from the repository's compose file, which mounts ./data to /updater/data and publishes port 8000. The Web UI on port 8000 then shows each record and its last update time.

### What is DDNS used for?

In this project's terms, it is used to keep DNS A and AAAA records pointed at your current public IP address across many DNS providers, checking periodically and calling the provider API when the address changes. It is aimed at self-hosted setups where a hostname must keep resolving after an ISP address change.

### How do I update my dynamic DNS?

Run ddns-updater as a container or binary with your records listed in data/config.json; it polls the public IP on the PERIOD interval, for example 5m, and calls the provider API only when the address differs from the stored value. The Web UI on port 8000 shows the last update time for each record.

### how to use ddns updater

Point the container at a data directory, put your provider entries in config.json, and let the scheduler run. Credentials are provider-specific, so check the configuration section for the fields your provider expects, and set SHOUTRRR_ADDRESSES if you want notification when an update fails.

### ddclient vs ddns-updater

Both poll for address changes and support many providers. ddclient is a distribution package with a Perl configuration file and no web interface, while ddns-updater is a Go binary or a 12MB Scratch-based container with a JSON config and a Web UI on port 8000.

## Sources

- [License: MIT](https://github.com/qdm12/ddns-updater/blob/master/LICENSE)
- [Project website](https://hub.docker.com/r/qmcgaw/ddns-updater/)
- [qdm12/ddns-updater on GitHub](https://github.com/qdm12/ddns-updater)
- [README](https://github.com/qdm12/ddns-updater/blob/master/README.md)
- [Releases](https://github.com/qdm12/ddns-updater/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/qdm12-ddns-updater
