Dockhand
Portainer has been the default Docker UI for years, and it shows — the interface carries a lot of history and a lot of Swarm and Kubernetes machinery most homelabs never touch. Dockhand is a newer take: the same core job, a modern interface, and a Compose editor that is genuinely pleasant to use.
Dockhand is a Docker management UI built on SvelteKit and Bun, running on a from-scratch Wolfi base image.
- Container management — start, stop, restart, with live status, resource use and port mappings
- Compose stacks with a visual YAML editor, environment variables side by side
- Git integration — deploy stacks from a repository, with webhooks and auto-sync
- Multi-environment — manage local and remote Docker hosts from one place
- Terminal and log streaming, plus a file browser into running containers
- OIDC SSO and local users, with RBAC in the Enterprise tier
Dockhand or Portainer?
Both mount the Docker socket and show you containers. The differences that matter:
| Dockhand | Portainer CE | |
|---|---|---|
| Age and ecosystem | New, smaller community | Years of guides and StackOverflow answers |
| Interface | Modern, quick | Functional, dated in places |
| Compose editing | Strong — side-by-side YAML and env | Workable |
| Git-backed stacks | Yes, with webhooks | Yes, with polling |
| Kubernetes | No | Yes |
| Docker Swarm | No | Yes |
| File browser into containers | Yes | No |
If you run Kubernetes or Swarm, Portainer covers ground Dockhand does not. For a Compose-only homelab, Dockhand is the nicer daily driver — and there is no reason not to run both against the same socket while you decide.
Anything with the Docker socket has root on the host
Dockhand mounts /var/run/docker.sock. A process that can talk to the socket can start a privileged container and take over the machine. That is a property of the socket, not a flaw in Dockhand — but it means this UI never faces the internet, and it gets a real password.
Prerequisites
- Docker and the Compose plugin — see the Docker guide
- Port 3000 free, or a willingness to remap it
1. The Compose file
Upstream's file:
services:
dockhand:
image: fnsys/dockhand:latest
container_name: dockhand
restart: unless-stopped
ports:
- 3000:3000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- dockhand_data:/app/data
volumes:
dockhand_data:
Port 3000 is crowded
Grafana, Karakeep and Dockhand all want 3000. On a host running more than one, remap the host side — 3010:3000 — and leave the container side alone.
2. Create the admin account
Browse to http://<host-ip>:3000. First run walks you through creating the initial administrator.
Use a real password. This account can deploy arbitrary containers on your host.
3. Look around
The local Docker environment is picked up automatically from the mounted socket. You should immediately see every container on the host, with live CPU and memory.
Worth knowing where things are:
- Environments — Docker hosts. One to start with; add more in step 5.
- Containers — status, ports, resources, plus the log stream and shell
- Stacks — Compose deployments, and where the editor lives
- Images — tags, sizes, update availability, and cleanup
4. Deploy a stack
Stacks → New Stack gives you the YAML editor. Paste a Compose file, set environment variables in the adjacent pane, deploy.
The editor is the reason to use Dockhand over the alternatives — env vars sit next to the YAML that consumes them, so ${THING} is not a guess.
Stacks created on the CLI stay on the CLI
Like Portainer, Dockhand can only manage stacks it has the Compose file for. Containers started with docker compose up from a shell appear under Containers, but not as an editable stack.
Pick one place per app and stay there. Two competing definitions of the same stack is how you lose an evening.
5. Add a second Docker host
Environments → Add Environment connects a remote host, so one UI covers the whole lab.
Connecting over TCP means exposing the Docker API, which is the socket problem again but over the network. Do not enable plain tcp:// on 2375. Either use an SSH connection, or mTLS on 2376 with certificates.
If you already run the Portainer agent on those hosts, that is a separate mechanism — Dockhand does not use it.
6. Git-backed stacks
Stacks → New → From Git points at a repository with a Compose file. With a webhook configured, a push redeploys.
Combined with a Gitea instance, that makes the repo the source of truth and the UI a viewer — which is the arrangement that survives contact with a lab you have not touched in six months. You can read what is deployed without logging into anything.
7. Put it behind Traefik
networks:
- traefik-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.dockhand.rule=Host(`dockhand.home.lan`)"
- "traefik.http.routers.dockhand.entrypoints=https"
- "traefik.http.routers.dockhand.tls.certresolver=letsencrypt"
- "traefik.http.services.dockhand.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-net"
networks:
traefik-net:
external: true
Drop the ports: block when you do this. Consider adding Tinyauth as a second factor in front — for a socket-mounting admin UI, a single password is thin.
Updating
Dockhand is young and moves quickly. Read the release notes before upgrading, and take the backup below first — this is not a project with years of upgrade-path scar tissue behind it yet.
Backup
docker compose stop
docker run --rm \
-v dockhand_dockhand_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/dockhand-$(date +%F).tar.gz -C /data .
docker compose start
The volume holds the database — users, environments and stack definitions. Git-backed stacks are reproducible from the repo; anything you pasted into the editor is not.
Troubleshooting
Blank dashboard, or no containers listed. The socket mount. docker exec dockhand ls -l /var/run/docker.sock.
Port 3000 already allocated. Something else has it. ss -tlnp | grep 3000, then remap the host side.
Remote environment will not connect. Check the Docker API is actually reachable and, if using TLS, that the certificates match the hostname you connected to.
Stack deploys but the container immediately exits. Read the container log, not the stack log — the stack succeeded in creating something that then failed on its own terms.
Cannot log in after an update. Check the logs for a migration error. Restore the backup if the database did not migrate cleanly.
Where this sits in my lab
Dockhand runs on the Docker VM on my first Proxmox node, alongside Portainer — both pointed at the same socket while I work out which one I actually reach for.
Early verdict: Dockhand for anything involving editing a Compose file, Portainer for the multi-host view, since its agent is already deployed everywhere. Neither is exposed beyond the LAN.