Skip to content

feat: Add Docker-based dynamic domain discovery#276

Closed
fullpipe wants to merge 7 commits into
timothymiller:masterfrom
fullpipe:docker-scan
Closed

feat: Add Docker-based dynamic domain discovery#276
fullpipe wants to merge 7 commits into
timothymiller:masterfrom
fullpipe:docker-scan

Conversation

@fullpipe

@fullpipe fullpipe commented Jun 1, 2026

Copy link
Copy Markdown

Hello,

Using ddns with docker most of the time. And it is annoying to update env vars when adding new local domains. So I made use of /var/run/docker.sock. And now it is possible to use ddns like this.

  ddns:
    image: timothyjmiller/cloudflare-ddns:latest
    restart: unless-stopped
    network_mode: host
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  api:
    image: "..."
    labels:
      - "ddns.domain=api-local.example.com"
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api-local.example.com`)"

@fullpipe

fullpipe commented Jun 1, 2026

Copy link
Copy Markdown
Author

P.S.: Have some issues with code formatting. A lot of fixes with simple cargo fmt

$ cargo -V
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
$ cargo fmt --version
rustfmt 1.9.0

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This RFC introduces Docker-based dynamic domain discovery so the updater can automatically pick up domains from running containers (via a ddns.domain label) instead of requiring manual DOMAINS env var updates.

Changes:

  • Adds a Docker scanner (using bollard) that periodically reads container labels and publishes an updated domain set through a tokio::sync::watch channel.
  • Threads the dynamic domain set into the update loop by changing updater::update_once to accept a domains map per cycle.
  • Extends env config with DOCKER_HOST to enable Docker-based discovery and updates tests/struct initialization accordingly.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/updater.rs Accepts per-cycle domains input and iterates over it for record updates.
src/main.rs Sets up a watch channel for domains and optionally spawns the Docker domain scanner.
src/docker.rs New module: scans Docker containers for ddns.domain labels and updates the watch channel.
src/config.rs Adds docker_host to AppConfig and loads DOCKER_HOST from env; updates validation/tests.
src/pp.rs Derives Clone for PP to allow passing it into spawned tasks.
Cargo.toml Adds the bollard dependency.
Cargo.lock Locks new transitive dependencies from adding bollard.
Comments suppressed due to low confidence (2)

src/updater.rs:121

  • If domains contains an IP type that has no configured provider (e.g., IP6_PROVIDER=none but domains include IPv6), detected_ips will always be empty and the code may proceed to delete records when DELETE_ON_FAILURE=true. This becomes more likely with Docker-discovered domains and can lead to unintended deletions.
        for (ip_type, domains) in domains.iter() {
            let ips = detected_ips.get(ip_type).cloned().unwrap_or_default();

            if ips.is_empty() && !config.delete_on_failure {
                ppfmt.warningf(

src/config.rs:545

  • The validation error message for having no update targets doesn’t mention DOCKER_HOST, even though it is now a valid way to configure DNS update targets. This makes misconfiguration harder to diagnose.
    if domains.is_empty() && docker_host.is_none() && waf_lists.is_empty() {
        return Err(
            "No update targets configured. Set DOMAINS, IP4_DOMAINS, IP6_DOMAINS, or WAF_LISTS."
                .to_string(),
        );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/updater.rs
Comment thread src/config.rs
Comment thread src/main.rs
Comment thread src/docker.rs
Comment thread src/docker.rs
Comment on lines +66 to +111
async fn scan_and_publish_once(
docker: &Docker,
static_domains: &HashMap<IpType, Vec<String>>,
docker_domains: HashSet<String>,
domains_tx: &mut Sender<HashMap<IpType, Vec<String>>>,
) -> Result<HashSet<String>, Error> {
let list_options = ListContainersOptionsBuilder::default()
.all(true)
.filters(&HashMap::from([(
"status",
vec!["created", "restarting", "running"],
)]))
.build();

let list = docker.list_containers(Some(list_options)).await?;

let new_docker_domains: HashSet<String> = list
.iter()
.filter_map(|c| {
c.labels
.as_ref()
.and_then(|labels| labels.get("ddns.domain"))
})
.cloned()
.collect();

if docker_domains == new_docker_domains {
return Ok(docker_domains);
}

let mut new_domains = static_domains.clone();

new_domains
.entry(IpType::V4)
.or_default()
.extend(new_docker_domains.clone());

new_domains
.entry(IpType::V6)
.or_default()
.extend(new_docker_domains.clone());

domains_tx.send_replace(new_domains);

return Ok(new_docker_domains);
}
fullpipe added 4 commits June 8, 2026 18:00
Introduce a background domain cleanup task and make notifiers clonable.

- Add spawn_domain_cleanup in docker.rs: spawns a task that watches domain changes and deletes Cloudflare records for domains removed from the running set when delete_on_stop is enabled (skips in legacy mode). It uses the CloudflareHandle to perform final_delete and sends messages via CompositeNotifier.
- Make CompositeNotifier cloneable by adding a CloneBox helper and changing NotifierDyn to require CloneBox; implement Box<dyn NotifierDyn> cloning. Update Shoutrrr notifier types to derive Clone.
- Minor CloudflareHandle tweaks: add Debug/Clone derive, small API call formatting cleanup, and various signature/format adjustments.
- Wire up spawn_domain_cleanup from main and adjust several call sites to borrow domains_rx instead of cloning unnecessarily.
- Lots of formatting and test adjustments (wrapping long lines, reformatting mocks) to accommodate the new code and maintain style.

These changes enable safe concurrent cleanup of DNS/WAF items when containers stop and allow notifier components to be duplicated for use inside spawned tasks.
@fullpipe fullpipe changed the title [RFC] Add Docker-based dynamic domain discovery Add Docker-based dynamic domain discovery Jun 10, 2026
@fullpipe fullpipe changed the title Add Docker-based dynamic domain discovery feat: Add Docker-based dynamic domain discovery Jun 10, 2026
@fullpipe

fullpipe commented Jul 8, 2026

Copy link
Copy Markdown
Author

Merged it to https://github.com/fullpipe/cloudflare-ddns if some one intrested

@fullpipe fullpipe closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants