A CLI tool that turns running Docker containers into static config files for self-hosted dashboards — Homepage, Homer, and Dashy — syncs them idempotently, and doesn't clobber whatever you edited by hand.
Status: pre-1.0, functional. Docker label discovery works, single- or multi-host. All three formats — Homepage, Homer, and Dashy — get the full idempotent merge (see docs/decisions/007).
Homepage and Glance already ship a built-in runtime Docker-label discovery,
and for most people that's enough. dashsync solves a different problem:
producing a static config committed to git — one you can review, roll
back, and deploy without giving the dashboard itself access to the Docker
socket.
The tool is built around three properties that follow from that:
- Idempotent. Two runs in a row with no changes in Docker produce a zero diff. The output is meant to be committed to git without noise.
- Managed markers. Generated entries are marked in the file, so a re-run only touches its own section: hand-written entries and comments are left alone, and services that disappeared get removed.
- Multi-format. One source of truth renders into several dashboard formats.
- No web UI, no daemon with a database, no Kubernetes.
- No Homarr support — its config lives in a database, so there's nothing to generate.
- Never writes to Docker. All access to the Docker API is strictly read-only.
Via Homebrew (macOS or Linux):
brew install nikitamikhailov/tap/dashsyncOr download a prebuilt binary from the latest release (linux/darwin, amd64/arm64):
os=$(uname -s)
arch=$(uname -m); case "$arch" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac
curl -fsSL "https://github.com/NikitaMikhailov/dashsync/releases/latest/download/dashsync_${os}_${arch}.tar.gz" \
| tar -xz dashsync
sudo mv dashsync /usr/local/bin/Every release also publishes a checksums.txt — download it from the same
releases page
and check the archive against it (sha256sum -c) if you want to verify the
download.
Or build it yourself:
go install github.com/NikitaMikhailov/dashsync/cmd/dashsync@latest(dashsync version on a go install-built binary shows the correct version
tag, but commit/date come back unknown — Go only embeds those from a
local git checkout's own VCS info, which a module-proxy install doesn't
have. A release binary gets both, via ldflags.)
Or run it as a container — a multi-arch (amd64/arm64) image is published
to ghcr.io/nikitamikhailov/dashsync on every release:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/nikitamikhailov/dashsync inspectAn ssh:// host (see docs/decisions/009)
needs a bit more mounted in, since the container starts with no SSH
history of its own — a known_hosts and a forwarded agent socket, not
just the config file:
docker run --rm \
-v /path/to/dashsync.yaml:/dashsync.yaml:ro \
-v ~/.ssh/known_hosts:/root/.ssh/known_hosts:ro \
-v $SSH_AUTH_SOCK:$SSH_AUTH_SOCK -e SSH_AUTH_SOCK \
ghcr.io/nikitamikhailov/dashsync inspect --config /dashsync.yamlSee docs/decisions/010 for why the image is built the way it is.
# List what dashsync would discover — no file written.
dashsync inspect
# Render Homepage's services.yaml to stdout.
dashsync sync
# Idempotently merge into an existing file: adds, updates, and removes
# managed entries; anything you wrote by hand is left alone.
dashsync sync --output-path services.yaml
# Homer and Dashy support the same idempotent merge, into their own
# formats (config.yml and conf.yml are each format's own conventional
# filename — --output-path accepts any path).
dashsync sync --format homer --output-path config.yml
dashsync sync --format dashy --output-path conf.ymlA container opts in with dashsync.enable=true; see
docs/decisions/001 for the full label
contract. For discovery across more than one Docker host (TCP, TLS, a
non-default socket, or SSH — ssh://user@host[:port], no separate
identity/agent config: it reuses your own ssh and ~/.ssh/config
exactly like docker -H ssh://... does, right down to needing a manual,
interactive ssh run once to accept a host's key before dashsync's own
non-interactive connection can reach it), point --config at a
dashsync.yaml describing them — see
docs/decisions/004 for
its schema and docs/decisions/009
for how the SSH case works; every subcommand's --help has the full flag
list.
dashsync has no git integration of its own — it just writes a plain
file, and that file happens to live in a directory you already keep in
git. That's the entire mechanism:
# Added a container, want the dashboard to catch up.
$ dashsync sync --output-path services.yaml
+ Media: Jellyfin (added)
1 change(s): 1 added, 0 updated, 0 removed, 0 conflict(s)
$ git diff services.yaml
+ # dashsync:managed id=4a0a65f45558 content=6ac06496
+ - Jellyfin:
+ href: http://10.0.0.5:8096
$ git add services.yaml && git commit -m "dashboard: add Jellyfin" && git pushFrom there it's an ordinary commit, reviewed and rolled back the same way
as any other config change — dashsync doesn't know or care that git is
involved. Two common ways to trigger the sync step itself:
- By hand, right after adding or changing a container — enough for a small setup.
- On a schedule (cron, a systemd timer), once "did anyone remember to
run sync" becomes a real question. A run that finds no changes exits
quietly; one that finds changes can auto-commit (fine for a low-stakes
personal setup) or open a pull request for a human to review before it
ships — worth it once a service silently disappearing from the
dashboard is something you'd want to catch before it goes live, not
after.
--dry-rundefaults to true whenever$CIis set, so a scheduled job meant to actually write needs--dry-run=false— an automation shell that happens to export$CIfor unrelated reasons will otherwise print a change summary and write nothing.
A separate, scheduled job — one that shouldn't write anything, just catch
a committed file going stale relative to what's actually running — wants
--check instead of the auto-commit/open-a-PR pattern above: it never
writes, and exits with a distinct code (2, not the generic 1 every other
failure gets) when the file has pending changes, so "the check found
drift" can be told apart from "the check itself broke." Since discovery
only ever reads Docker's current state (dashsync never inspects a
compose file or a PR diff — see "Why this exists" above), this job needs
to run somewhere that can already see the container in question, which
makes it a post-deploy drift check, not a pre-merge PR gate: a container
added in a PR isn't running anywhere yet for --check to discover until
after that PR merges and deploys.
dashsync sync --output-path services.yaml --checkEither way, the dashboard itself only ever reads the rendered file off
disk — it never talks to Docker and doesn't care whether dashsync or a
human wrote what it's looking at.
sync --output-path also leaves a <output-path>.lock file next to it,
permanently — an empty sidecar used to keep two overlapping runs from
clobbering each other (see ADR 008).
It carries no content worth committing; add it to your own .gitignore.
This locking is best-effort, not a guarantee: it relies on the output
path living on an ordinary filesystem. A directory bind-mounted into a
container through certain virtualized filesystem layers (confirmed
against Docker Desktop's virtiofs specifically) can let two writers each
believe they hold the lock at once, silently. If dashsync itself runs
inside a container writing to a bind-mounted config directory, keep
overlapping schedules serialized yourself rather than relying on this —
see ADR 008 for why there's no portable fix available at this layer.
go build ./cmd/dashsync
go test ./... -race
golangci-lint runApache-2.0. Chosen for its explicit patent grant: this is an infrastructure tool that could end up inside a corporate environment, and that's usually the first question asked there.