A clean terminal-based Docker manager for self-hosted servers. Monitor Compose projects, check image updates, safely upgrade containers, and clean up leftover data.
Zero dependencies: just Python 3.8+ and Docker.
curl -fsSL https://raw.githubusercontent.com/ts0m1s/Docky/main/install.sh | shThis installs to ~/.local/share/docky, links ~/.local/bin/docky, and sets up tab completion. To uninstall, delete those two paths and the two # Docky tab completion lines at the end of ~/.zshrc / ~/.bashrc.
In zsh and bash, press Tab after docky to see every command with its description. It also completes what comes next:
❯ docky restart <Tab> all arr gluetun jellyfin seerr traefik …
❯ docky remove arr --<Tab> --volumes --images --files --all --dry-run --yes
❯ docky rollback arr <Tab> services with a saved snapshot
Project names are looked up live, so new stacks appear immediately. A folder path (/, ~, .) completes as a path. The installer adds one line to the end of ~/.zshrc (and ~/.bashrc if you have one) that loads the script; docky self-update keeps the script current. Set DOCKY_NO_MODIFY_PATH=1 before installing to get the line printed instead, or print the script yourself with docky completion zsh / docky completion bash.
Mistyped commands get a suggestion: ! Unknown command: self-udpate Did you mean docky self-update?
docky self-update # install the latest version
docky self-update --check # just show what's new
docky --version # what's installedself-update follows the main branch and lists the changes since your version. The new version is downloaded and compiled in a temporary folder first, then swapped in, so a failed download never leaves Docky half-updated. If you installed as root, run it with sudo.
Once a day, after a command finishes, Docky prints a one-line notice when a newer version exists. It never updates on its own. Turn the notice off with export DOCKY_NO_UPDATE_CHECK=1.
Installs from before self-update existed don't record their version. Run the install command once more, and docky self-update works from then on. To follow a different branch, install with DOCKY_REF=<branch>; self-update keeps following it.
Docky has version numbers (docky --version → docky 0.2.0 (a3b20d8, …)), and every published version is a GitHub release with notes.
-
main (default): every change as soon as it's merged.
-
stable: only published releases. Install with
DOCKY_REF=stable:curl -fsSL https://raw.githubusercontent.com/ts0m1s/Docky/main/install.sh | DOCKY_REF=stable shOn this channel the daily notice reads
Docky 0.3.0 is available (you have 0.2.0), andself-updatelinks to the release notes. Pre-releases (v0.3.0-rc.1) are never offered.
-
In a PR, bump
__version__indocky/__init__.py(0.2.0→0.3.0for new features,0.2.1for fixes only), then merge it. -
Tag the merge and push the tag:
git checkout main && git pull git tag v0.3.0 && git push origin v0.3.0
-
The release workflow checks that every module compiles and the tag matches
docky/__init__.py, then publishes the release with notes generated from the merged PRs. Tagv0.3.0-rc.1to publish a pre-release for testing first.
docky <command> [target]
| Command | Description |
|---|---|
projects |
List every project Docky found, where it lives, and the compose files it uses |
urls [name] [--check] |
Show where each service is reachable: domains from Traefik, LAN and Tailscale addresses from published ports. --check tests every URL |
status |
Show Docker projects, containers, and system metrics. Flags containers whose shared network (network_mode: container:...) points at a container that no longer exists |
top [--sort cpu|mem] |
Live view per container: CPU, memory (and how close it is to its limit), network and disk as per-second rates. Columns fit the terminal; --sort puts the heaviest projects and containers first |
updates |
Check for available image updates, showing the version change (e.g. 4.0.20-ls325 → 4.0.20-ls326) and a release-notes link |
upgrade [name] [--dry-run] |
Pull and recreate outdated containers, verifying health, and list each version change. Give a project name to upgrade just that one; --dry-run shows the plan without changing anything. Services that share an upgraded service's network (network_mode: service:X) or depend on it with restart: true are recreated along with it, so e.g. qBittorrent behind gluetun keeps its connection. rollback does the same |
rollback [name] [service] |
Undo the last upgrade. Docky saves the previous image before every upgrade; with no arguments this lists the saved snapshots |
sweep |
Find and clear stopped containers and unused images |
orphans |
Find volumes belonging to deleted or renamed projects |
remove <name|path> [--volumes] [--images] [--files] [--all] [--dry-run] [-y] |
Remove a project completely. See Removing a project |
start / stop / restart <name|all> |
Control a project or all of them |
Your stacks can live anywhere. Docky finds them in two ways:
-
From Docker itself. Every container Compose starts is labelled with its project name, folder and compose files. Any project with at least one container (running or stopped) is found automatically, wherever its folder is.
-
By scanning folders, for projects that exist on disk but haven't been started yet. By default Docky looks in
~/docker, up to two levels deep. To use other folders, setDOCKY_ROOTto one or more paths separated by::export DOCKY_ROOT=/opt/stacks:/srv/compose:~/homelab
Run docky projects to see what was found and from where. Commands take a project name, or its folder path when two projects share a name (docky restart /opt/stacks/app).
Docky runs Compose with the same files the project was started with, so docker-compose.override.yml and friends are kept. If a project's folder is moved or deleted while its containers still exist, docky status warns about it instead of guessing.
docky urls lists where each container can be reached, using only what Docker already knows:
- Domains come from Traefik router labels (
Host(...), includingPathPrefix). A router with TLS or awebsecureentrypoint is shown ashttps://. - LAN and Tailscale addresses come from published ports. A port bound to
0.0.0.0is shown on the machine's LAN IP and its Tailscale name. A port bound to127.0.0.1is marked "this machine only". - Containers sharing another's network (
network_mode: service:gluetun) are reached through that container. A Traefik router named after the service is shown under it, so qBittorrent's URL appears under qBittorrent, not under the VPN. - When Traefik labels name the web port, other published ports (such as a torrent port) are listed as "published", not as URLs.
docky urls --check opens every URL and marks it ✓ or ✗ (doesn't resolve, refused, timed out, 5xx). A login page (401/403) counts as reachable. Use it to see which domains actually work from this machine; for example, a Cloudflare tunnel's DNS record may be missing.
updates, upgrade --dry-run and upgrade show what each update changes, read from the image's labels:
├─ ↑ sonarr update available 4.0.20.3014-ls325 → 4.0.20.3014-ls326
├─ ↑ homarr update available built 2026-09-18 → built 2026-09-29
└─ ✓ radarr up to date · 6.4.4.10685-ls318
- No pulling: the registry's version is read without downloading the image, straight from the registry over HTTPS (or
docker buildx imagetoolsfor registries that need your credentials). - Where the version comes from: the
org.opencontainers.image.versionlabel, then linuxserver.io'sbuild_version. For Docker Hub images whose labels only saylatest/main, the version tag pointing at the same image (e.g.latest=0.21.0). Otherwise, the build date. - Remembered: a version found once is saved for that image digest (
~/.local/state/docky/remote-versions.json), so later checks only ask whether the tag moved. - Rate limits: if Docker Hub's anonymous pull limit is used up, Docky reads versions from hub.docker.com instead, which isn't affected.
docker loginraises the limit, and Docky uses that login automatically. If a version truly can't be read, the row says why instead of showing?. - The "from" side is the image the container is actually running. A newer image that was pulled but never applied is reported as an update.
- Summary: the run ends with the version changes and, when the image names its GitHub/GitLab/Codeberg source, a release-notes link.
docky remove <name> first shows a plan of everything that belongs to the project, then asks before each part:
- Always removed: the project's containers (running ones are stopped) and the networks Compose created for it.
- Optional:
--volumes: its named and anonymous volumes. The data is gone for good.--images: images no other container uses, plus Docky's rollback snapshots for it.--files: the project folder (compose files,.env, and any data stored inside it).
- Never touched: external or shared volumes and networks, images other projects use, and bind-mounted data outside the project folder. These are listed in the plan so you know what stays.
Without flags, Docky asks about each optional part. Deleting volumes or the folder requires typing the project name. --all selects everything, --dry-run shows the plan and changes nothing, and -y skips the prompts (only what you flagged is removed).
Safety rails:
- The folder is never offered if it's your home folder, a
DOCKY_ROOTscan folder, or contains another project. - If any file in the folder can't be deleted (for example data a container wrote as root), nothing in the folder is deleted, and Docky prints the
sudo rm -rfcommand to finish. - Projects whose folder was already moved or deleted can still be removed by name.
docky.py the `docky` command: a small launcher for the package below
docky/
__init__.py version number
cli.py argument handling, help, the command list (also drives tab completion)
commands/ one module per area: status, updates, cleanup, lifecycle, remove
docker_api.py everything that talks to Docker / Compose
monitor.py the live `docky top` view
urls.py where services are reachable (`docky urls`)
versions.py image versions for `docky updates`
completion.py zsh / bash tab completion
selfupdate.py `docky self-update` and the daily new-version notice
utils.py colours, shell commands, terminal helpers
install.sh the installer
.github/workflows/ the release workflow
Run it from a checkout with python3 docky.py <command>; no install needed.
MIT