Dockhand: One Docker Dashboard for a Multi-Host Homelab
Most homelabs end up running Docker on more than one box eventually. Mine does: a Synology NAS for the apps that want tight NAS integration, and a separate Ubuntu server for everything else. The problem that creates isn't Docker — it's that you now have several completely separate places to check when something's wrong, several different ways of pushing an update, and no single view of "what's actually running right now." Dockhand is what closed that gap for me.
What Dockhand actually is
Dockhand is a self-hosted Docker management UI — think Portainer's job, but built around two things I care about more: multiple environments in one pane of glass, and a git-backed deploy model instead of clicking around in a UI to define stacks by hand.
Point it at as many Docker hosts as you run (in my case, the NAS as environment id 1 and my secondary server as id 2) and you get one dashboard listing every stack, every container, health status, restart counts, and logs — regardless of which physical box it's actually running on. No more "which server was that container on again?"
Getting it running
Dockhand itself deploys like any other compose-based app — a single container, pointed at the local Docker socket so it can talk to that host's Docker daemon:
services:
dockhand:
image: ghcr.io/dockhand-app/dockhand:latest
container_name: dockhand
restart: unless-stopped
ports:
- "9202:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- dockhand-data:/data
environment:
- TZ=Europe/Amsterdam
volumes:
dockhand-data:
That's enough to manage the local host. Additional hosts don't need another compose entry — you add them as extra environments from inside the Dockhand UI (each one just needs its own reachable Docker API endpoint), which is what turns a single-host viewer into the one-pane-of-glass dashboard.
Why the Gitea-first flow matters
The part that changed how I actually work is that my secondary server's stacks are git
stacks: the compose.yaml/.env for each app lives in a Gitea repo, and Dockhand polls
that repo for changes. My deploy loop for any app there is:
- Edit
compose.yamlin the repo (synced to a real git remote). - Commit and push to
main. - Dockhand's poller (or a webhook trigger) picks up the change and redeploys the stack.
- Confirm the deploy actually applied and the container is healthy.
That means the config change and the deploy are the same action — push to git. There's no "I edited the file on the host directly and now the repo doesn't match reality" drift, which is the exact failure mode that makes homelabs rot over time. If I ever need to know what's actually running on the box, the repo already tells me.
The NAS's stacks work a little differently — they're internal stacks (backed by
compose files that live directly on the NAS, not pulled from git), but Dockhand still
gives me a GET .../compose endpoint to read the live on-host file before I push a
change, so I can confirm a workspace edit actually reached the host before triggering a
deploy there too.
Monitoring, updates, and cleanup — without touching a shell
The dashboard side is more than a status list. Each container shows live CPU and memory
usage, so "is this thing quietly eating all the RAM" is a glance, not a docker stats
session over SSH. Combine that with per-container health and restart counts and you get
an actual early-warning signal — a container that's restart-looping or creeping in memory
shows up before it takes something else down with it.
Dockhand also flags when a newer image is available for a running container, and lets you
pull and redeploy that update straight from the UI (or the API) instead of SSHing in to
docker compose pull && docker compose up -d by hand. For git-backed stacks that's still
just a compose-file bump and a push; for the NAS's internal stacks, it means the update
button is the whole workflow.
And because it's a real Docker control plane, not just a viewer, cleanup is built in too: prune dangling images, see exactly how much disk each image is costing you across every host, and reclaim space without ever working out which host actually needs it — you just look at the dashboard and prune from there.
The part that made this genuinely nice to automate: the API
Dockhand isn't just a UI — it has a full REST API, and that's what turned "check on the homelab" from an SSH session into something I can script or hand to an agent:
GET /api/environments— list the registered Docker hosts.GET /api/stacks?env=<id>— every stack on that host, with per-containerstate,health,image,restartCount, and whether an update is available.GET /api/containers/{id}/logs?env=<id>&tail=N— pull recent logs for any container, on either host, without opening a shell.POST /api/stacks/{name}/deploy?env=<id>— re-runcompose up -dfor a stack. Because it's a plainup -d, it only recreates the services whose config actually changed and reports the rest as already running — safe to call even for a one-line change in a multi-service stack.GET /api/images?env=<id>plusPOST /api/images/prune?env=<id>— see what's eating disk space across every host and clean it up, again without SSH.
In practice this means routine homelab admin — "is everything healthy," "did that update actually apply," "why is this container unhealthy," "clean up dangling images" — no longer needs a host shell at all. I only drop to SSH when a task genuinely needs host-level access the API can't reach.
What I'd tell someone setting this up
- Register every environment before you do anything else. The whole value of Dockhand is the single pane of glass — if you only ever add one host, you're paying for a UI Portainer already gives you for free.
- Decide git-backed vs. internal per stack, deliberately. Git-backed stacks give you history, review, and drift-proofing for free; internal stacks are simpler for things that are genuinely NAS-native and unlikely to move. Don't fight the tool by forcing everything into one model.
- Use the API for anything you do more than twice. The UI is fine for a one-off look, but the moment you're checking the same three things every session, script it against the API instead — it's faster and it's the same thing an automation/agent workflow would use anyway.
- Trigger a deploy, then re-check state — don't assume success. A deploy call returns per-container output, but the healthcheck result is what actually matters; always follow up with a stacks/status call before calling something done.
Every host, one dashboard, and a deploy flow that's just "push to git" — that's the whole pitch. It's not flashy, but it's the piece of the stack that made the rest of this homelab feel like one system instead of several.