docker-dns-sync is a Go daemon that watches Godoxy-labeled Docker workloads and keeps daemon-owned AdGuard Home DNS rewrites in sync.
- Install from Homebrew with
brew install --cask aaronflorey/tap/docker-dns-sync. - Download the latest release archive for your platform from GitHub Releases and place
docker-dns-syncon yourPATH. - Build from source with
mise install && mise exec -- go build -o bin/docker-dns-sync ./cmd/docker-dns-sync. - Pull the release container image from
ghcr.io/aaronflorey/docker-dns-sync:<tag>.
- Performs a full reconcile on startup using Docker snapshot state and the persisted ownership file.
- Watches Docker for container lifecycle hints and reruns reconciliation after changes.
- Retries temporary AdGuard failures with bounded backoff before giving up.
- Only deletes stale remote rewrites when exactly one visible record carries non-empty daemon-owned provenance; same-key records without provenance, ambiguous duplicates, and unrelated manual records are preserved instead of being deleted on label changes alone.
The binary contract is:
docker-dns-sync -config /etc/docker-dns-sync/config.toml
Start from testdata/config/example.toml for a host-binary deployment with a local Docker socket, config.example.toml or testdata/config/docker-container.toml for a Docker container deployment that mounts /var/run/docker.sock, or testdata/config/socket-proxy.toml for a TCP proxy or remote Docker source.
Required config sections:
[[sources]]for Docker connectivity.[[outputs]]for AdGuard Home connectivity.[state]for the writable ownership snapshot path.[logging]for log level and format.[runtime]for per-operation timeouts on provider reads and writes.[retry]for bounded backoff settings.
[runtime] operation_timeout defaults to 10s. It applies per source/output read or write call (ListDesired, ListVisible, Create, Update, Delete), not to Watch(ctx) streams and not to the full reconcile pass. Retry backoff starts only after an operation returns or times out.
For Docker sources, set sources[].host_ip to the host-reachable IP address that unlabeled records should answer with. Explicit proxy.<alias>.host, proxy.#<n>.host, and proxy.*.host labels still override that shared source IP.
Use password_ref = "ENV:ADGUARD_PASSWORD" instead of embedding credentials in committed config.
- Install the pinned Go toolchain with
mise install. - Run static checks with
mise exec -- go vet ./.... - Run unit tests with
mise exec -- go test ./.... - Run opt-in live Docker verification with
mise run live-test. - Live verification requires
docker,docker compose, andcurl;digis optional because the script falls back to containerizednslookup. - The live test is not part of the default Docker-free
go test/go vetpath and cleans up withdocker compose ... down -vplus temporary runtime directory removal unless--keep-runningorKEEP_RUNNING=1is used; keep-running mode prints the manual cleanup commands you must run after inspection. - Keep secrets out of logs and shell history: use env references, do not paste real credentials inline, and avoid dumping compose logs by default.
- Stale remote cleanup is intentionally conservative: deletion requires unique daemon-owned provenance that the output can still see. Providers that cannot expose that proof, such as current AdGuard rewrite listing, leave same-key records in place rather than risk deleting operator-managed DNS.
- Renovate is configured through
renovate.jsonfor Go modules, GitHub Actions, and container image references.
- Pushes to
mainandmasterrun CI and update the release-please PR. - Merging the release PR creates a
vX.Y.Ztag and GitHub release. - Release publishing uploads macOS and Linux archives plus checksums.
- The same tagged GoReleaser run publishes multi-arch container images to GHCR.
- The same tagged GoReleaser run updates the Homebrew cask in
aaronflorey/homebrew-tap. - The release workflow requires a
HOMEBREW_TAP_GITHUB_TOKENsecret with push access toaaronflorey/homebrew-tap.
- Build the binary with
mise exec -- go build -o bin/docker-dns-sync ./cmd/docker-dns-sync. - Install the binary to
/usr/local/bin/docker-dns-sync. - Copy
testdata/config/example.tomlto/etc/docker-dns-sync/config.tomlfor a local Docker socket source, or start fromtestdata/config/socket-proxy.tomlif Docker is reachable through a proxy or remote endpoint. Keep a writable state path such as/var/lib/docker-dns-sync/state.json. - Create an environment file at
/etc/docker-dns-sync/docker-dns-sync.envwithADGUARD_PASSWORD=.... - Install
deploy/systemd/docker-dns-sync.serviceto/etc/systemd/system/docker-dns-sync.service. - Run
sudo systemctl daemon-reload && sudo systemctl enable --now docker-dns-sync.service.
Notes:
- The sample unit intentionally does not require
docker.service, so it also works when the Docker source usestcp://...instead of the local socket. - If you use
/var/run/docker.sock, the service user must be able to read and write that socket. A common host setup is to add the service user to the group that owns the socket and uncommentSupplementaryGroups=dockerin the sample unit if that group name isdocker. - The state file path must be writable by the service user.
- Restart behavior is explicit in the unit file:
Restart=on-failure.
- Pull a release image with
docker pull ghcr.io/aaronflorey/docker-dns-sync:<tag>, or build locally withdocker build -t docker-dns-sync .. - Use the root
docker-compose.yamlplusconfig.example.tomlfor the simplest container deployment, or start fromtestdata/config/docker-container.toml/testdata/config/socket-proxy.tomlfor a custom deployment. - Create a writable host directory for state such as
/opt/docker-dns-sync/state. - Provide the AdGuard password through an environment variable.
- Run the container:
docker run -d \
--name docker-dns-sync \
--restart unless-stopped \
-e ADGUARD_PASSWORD=replace-me \
-v /opt/docker-dns-sync/config.toml:/etc/docker-dns-sync/config.toml:ro \
-v /opt/docker-dns-sync/state:/var/lib/docker-dns-sync \
-v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/aaronflorey/docker-dns-sync:<tag> \
-config /etc/docker-dns-sync/config.tomlNotes:
- The container image runs as root on purpose so the documented
/var/run/docker.sockmount works with the default host socket ownership and mode used by Docker installations. - For local Docker socket sources, set
host_ipin the source config to the host-reachable address you want all derived records to answer with. Useproxy.<alias>.host,proxy.#<n>.host, orproxy.*.hostonly when a specific container needs a different answer target. testdata/config/example.tomlis intentionally host-oriented and leaves AdGuard at127.0.0.1; do not reuse it unchanged inside the container unless AdGuard is reachable at container-local localhost.- Mounting
/var/run/docker.sockgives the container broad Docker control. If you do not want to grant that access, use atcp://...endpoint such astestdata/config/socket-proxy.tomland give the container only the network path to that proxy or remote daemon. - If your host uses rootless Docker or a non-standard socket owner, adjust the container user or socket permissions to match that environment before mounting the socket.
- Keep the state directory persistent across restarts so ownership recovery works.
- The container image runs the real CLI contract with
-configand does not carry built-in config defaults.
testdata/config/example.toml- env-ref based example config.config.example.toml- root-level compose-oriented example config.docker-compose.yaml- root-level container deployment example.testdata/config/docker-container.toml- env-ref based Docker container config for/var/run/docker.sockplus network-reachable AdGuard.testdata/config/socket-proxy.toml- env-ref based TCP proxy or remote Docker example config.deploy/systemd/docker-dns-sync.service- starting point for host deployment.Dockerfile- local container build for Docker deployment.Dockerfile.release- GoReleaser container build for published multi-arch images.
See CONTRIBUTING.md for local setup and pull request expectations.
Released under the MIT License. See LICENSE.