Last 12 weeks · 2 commits
2 of 6 standards met
Disclaimer: I use Claude Code to implement parts of this PR, especially the json state functionality This PR adds functionality to remove orphaned DNS records. Why? This is very useful when using the option and service discovery modules like, in my case, caddy-docker-proxy. Currently we would need to either clean up the DNS records by hand or leave them registered. I propose to solve this by keeping track of our managed domains in a state file () and calling to remove orphaned records. To do so, we need to add the option: This is fully backwards compatible, so without nothing will be touched. I found no other way then to use a state file, since: Tracking all DNS records in zone would delete manually created records of that zone -> Dangerous! Saving the state In-memory only -> Doesn't survive restarts and may leave orphans again Claude suggested to using it makes sense and I couldn't find any other method already present in caddy to save the state. Is this correct? I tested this with my own provider and the provider. Both work flawlessly. I tried to encapsulate the change and to always fail gracefully. I also added test for the storing the state and the deletion functionality. We got some limitations: Removing an entire zone from config prevents orphan cleanup for that zone Changing won't clean up records of the disabled type Multiple Caddy instances sharing storage may cause state conflicts That I added to the . I'm open for feedback! Edit:** One thing to add, I could only test IPv4 since my ISP is not offering IPv6 -.-
This allows updating domains across multiple DNS providers — or multiple accounts of the same provider (e.g. different API credentials per zone) — from a single app. Repeating the whole global option doesn't work (Caddy keeps only the last occurrence), so the app itself now holds a list of providers. Config The directive can now be repeated, with each provider's domains nested inside its block. Caddyfile config (global options): Equivalent JSON config: All other options (, , , /, , , ) remain global and apply to all providers, so no existing option changes semantics. The public IP is looked up once per check and shared across providers. (Per-provider variants of e.g. could be follow-ups if there's demand.) Implementation notes Caddyfile parsing:* the parser takes the provider directive's segment via , splits out sub-blocks, and hands the remaining tokens to the DNS provider module through a rebuilt dispenser — so provider modules parse exactly the tokens they'd parse today. is the only name reserved inside a provider block; it seemed safe to claim since providers receive zones at runtime via libdns rather than via config, any collision would be a loud parse error rather than silent misbehavior, and JSON config bypasses the Caddyfile entirely. Backward compatibility: top-level / are folded into a one-entry list at provision time, and a legacy Caddyfile still adapts to identical JSON as before (covered by regression tests). Mixing top-level with per-provider is rejected with a clear error. Seeding: looking up current IPs from existing DNS records is now tracked per domain name instead of once globally, so every provider's domains get seeded — and as a side effect, domains added by a config reload now get seeded too (previously they were skipped if any earlier config had already seeded). Error semantics unchanged: the boot-time retry backoff still triggers only on total IP-source failure; per-provider failures are logged, as before, so one provider's bad credentials can't wedge the check loop. Testing New parser tests using a dummy in-test DNS provider module (which errors on unrecognized subdirectives — proving reserved tokens never leak into the provider module), covering multi-provider configs, before/after module config in the block, and legacy-format regressions. Unit tests for the legacy config normalization and its error cases. Built with xcaddy + caddy-dns/glesys: produces the expected array, and a legacy Caddyfile produces byte-identical JSON to master. Live test:* ran one Caddy instance against 4 real GleSYS accounts (4 zones, different API credentials each). All four seeded from existing records, created A+AAAA records, verified in public DNS, and subsequent cycles correctly no-op'd. On master, only the last-configured provider ever acts.
Repository: mholt/caddy-dynamicdns. Description: Caddy app that keeps your DNS records (A/AAAA) pointed at itself. Stars: 366, Forks: 38. Primary language: Go. Languages: Go (100%). License: Apache-2.0. Open PRs: 6, open issues: 10. Last activity: 2w ago. Community health: 42%. Top contributors: mholt, francislavoie, dependabot[bot], jm355, caiych, otaconix, cobyge, eliasvasylenko, asio, matthewpi and others.