Keeps a DuckDNS record on your current IP — and reports unhealthy when it stops
436
Keeps a DuckDNS hostname pointed at your current public IP. One small container, no dependencies, and — the reason it exists — it reports unhealthy when it stops working.
linux/amd64 + linux/arm64 · ~12MB compressed · runs as uid 10001, no privileges
docker run -d --name duckdns --restart unless-stopped \
-e DUCKDNS_DOMAINS=myhost \
-e DUCKDNS_TOKEN=your-token \
georgegozal/duckdns-updater:1
DUCKDNS_DOMAINS is comma-separated and takes the subdomain only, without
.duckdns.org: myhost,another,third.
The usual inline version is a while loop around curl, and it mostly works.
What it cannot do is fail:
entrypoint: [sh, -c, 'while :; do curl -fsS "https://www.duckdns.org/update?..."; sleep 300; done']
DuckDNS answers HTTP 200 for both success and failure, distinguishing them
only by a response body of OK or KO. So curl --fail succeeds on a rejected
update, the loop prints KO, sleeps, and the container stays contentedly
running — forever.
That matters because of what depends on this record. On a residential connection it is how Let's Encrypt finds your host at renewal time, roughly every 60 days. A stale record means the challenge reaches the wrong address and renewal fails — and with an inline loop you find out when the certificate expires, months after the cause.
This image checks the response body, records the last successful update, and
fails its healthcheck when that becomes too old. docker ps shows the problem
while there is still time to fix it.
| Variable | Default | |
|---|---|---|
DUCKDNS_DOMAINS | required | Comma-separated subdomains, no .duckdns.org |
DUCKDNS_TOKEN | required | Your DuckDNS token |
DUCKDNS_TOKEN_FILE | — | Read the token from a file instead — a Docker secret, or a tightly-permissioned mount |
INTERVAL | 300 | Seconds between updates |
RETRIES | 3 | Attempts per round, on network failure only |
DUCKDNS_IPV6 | — | An AAAA address to set alongside the A record |
STATE_DIR | /var/lib/duckdns | Where the last-success timestamp lives |
TZ | UTC | Timezone for log timestamps |
Whitespace is stripped from the token. A trailing newline — which a token pasted
into an .env file or a secret usually keeps — otherwise produces a bare KO
with no hint about why.
The public IP is never looked up. ip= is sent empty, so DuckDNS uses the source
address it sees. That is both correct behind NAT and means no third-party
"what is my IP" service is involved.
The token is never written to the logs.
HEALTHCHECK interval=60s timeout=10s start-period=30s retries=2
Unhealthy once the last success is older than INTERVAL * 3 — enough tolerance
that one failed round does not flap the status, soon enough that a persistent
failure surfaces long before a certificate renewal comes due.
docker inspect --format '{{.State.Health.Status}}' duckdns
A KO is treated as a configuration error and is not retried: retrying
cannot fix a wrong token, and hammering DuckDNS about it is rude. Network
failures are retried with a short backoff instead of waiting a whole interval.
services:
duckdns:
image: georgegozal/duckdns-updater:1
container_name: duckdns
restart: unless-stopped
environment:
DUCKDNS_DOMAINS: ${DUCKDNS_DOMAINS:?}
DUCKDNS_TOKEN: ${DUCKDNS_TOKEN:?}
volumes:
# Keeps the last-success timestamp across restarts, so a restart does not
# reset the health window and hide a problem that was already showing.
- duckdns_state:/var/lib/duckdns
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
volumes:
duckdns_state:
1 | Use this one. Latest 1.x — patches automatically, never a breaking change |
1.0.0 | Exact version, never moves |
latest | Latest overall, including future major versions |
Content type
Image
Digest
sha256:95d077d8e…
Size
5.9 MB
Last updated
16 days ago
docker pull georgegozal/duckdns-updater