Self-hosted dynamic DNS server: DigitalOcean, Vultr, Cloudflare, Azure DNS, Route53. HTTP + CLI.
1.5K
A self-hosted dynamic DNS server. It wraps several DNS provider APIs behind one simplified interface and exposes that through two interchangeable front-ends:
Both go through the same code path, so behaviour never diverges between them.
$ curl -H "Authorization: Bearer $TOKEN" https://ddns.example.com/v1/hosts/home/update
{
"host": "home",
"fqdn": "home.example.com",
"status": "updated",
"changed": true,
"records": [
{ "type": "A", "status": "updated", "ip": "203.0.113.99", "previous": "203.0.113.77" }
],
"client_ip": "203.0.113.99"
}
Most DDNS clients are tied to one provider and one delivery mechanism. This one separates the three concerns:
DdnsUpdater, shared by HTTP and CLI.${ENV_VAR} placeholders
is the whole configuration, so it can be version-controlled and the secrets
injected at runtime.The unchanged short-circuit matters most in practice: a record already pointing at the right address costs one read and zero writes, which is what makes a 60-second poll interval safe against provider rate limits.
| Driver | Status | Notes |
|---|---|---|
digitalocean | Available | Domain Records API, fully paginated |
vultr | Available | DNS API v2, cursor pagination |
cloudflare | Available | API v4; zone IDs resolved once and cached |
azuredns | Available | Azure public DNS zones, via the management REST API |
azureprivatedns | Available | Azure Private DNS zones |
route53 | Available | AWS SDK for PHP; supports the full AWS credential chain |
$ ddns providers:list
Adding one is a single class plus a factory — see Adding a provider.
Two stacks, one image:
| File | Purpose |
|---|---|
compose.yaml | Production — hardened, restart policies, no source mounts |
compose.dev.yaml | Development — source bind-mounted, dev dependencies, toolchain |
Requires Compose v2.24 or newer (for env_file: required:), invoked as
docker compose. The standalone docker-compose v1 reached end of life in
July 2023 and cannot parse these files.
On the file names.
compose.yamlis the canonical name in the Compose Specification; Docker's docs list it as preferred and treatdocker-compose.ymlas supported only "for backwards compatibility of earlier versions"1. Compose findscompose.yamlby default, which is why the production commands below need no-f.
Production:
cp config/ddns.example.yaml config/ddns.yaml # edit: zone, record, provider
cp .env.example .env # edit: API token, host token
docker compose up -d # HTTP endpoint
docker compose --profile watcher up -d # or poll from inside your network
The server binds to 127.0.0.1:8080 by default, on the assumption that a
reverse proxy terminates TLS in front of it. Set DDNS_HTTP_BIND=0.0.0.0:8080
to expose it directly. The container runs as uid 1000 with a read-only root
filesystem and all capabilities dropped.
Development: ./bin/ddns config:init --sample once, then
docker compose -f compose.dev.yaml up — see
Running it locally.
The two stacks use different Compose project names (ddns and ddns-dev), so
a dev stack can never collide with a production one on the same host.
devis the last stage in theDockerfile, so a plaindocker build .produces the development image. Pass--target runtimefor production.
Without cloning the repository: the same image is published to both
ghcr.io/dmanavi/ddns and dmanavi/ddns on Docker Hub, tagged with the full
version (for example 1.20260818.2157), major.minor (the release date),
major and latest. Fetch just the two files a container needs and run it
directly:
mkdir ddns && cd ddns
curl -O https://raw.githubusercontent.com/DManavi/ddns/main/config/ddns.example.yaml
mv ddns.example.yaml ddns.yaml # edit: zone, record, provider
curl -O https://raw.githubusercontent.com/DManavi/ddns/main/.env.example
mv .env.example .env # edit: API token, host token
docker run -d --name ddns \
--env-file .env \
-v "$(pwd)/ddns.yaml:/config/ddns.yaml:ro" \
-p 127.0.0.1:8080:8080 \
ghcr.io/dmanavi/ddns:latest
# or dmanavi/ddns:latest — same image, the other registry
Prefer compose.yaml for anything longer-lived than a quick check: it adds the
read-only root filesystem, dropped capabilities and restart policy this plain
docker run does not. It still needs cloning for the file itself, but not for
an image build — point it at the published image instead of building:
DDNS_IMAGE=ghcr.io/dmanavi/ddns:latest docker compose up -d
The rest of the documentation - configuration reference, HTTP API, CLI, provider setup, security notes and more - is in the full README on GitHub: https://github.com/DManavi/ddns#readme
Content type
Image
Digest
sha256:01fe78527…
Size
37.7 MB
Last updated
about 1 month ago
docker pull dmanavi/ddns