Live, keyboard-driven terminal UI for Docker: Compose-aware, real-time stats, zero polling — manage containers, images, volumes, and networks without leaving the terminal
Project description
docksurf
A keyboard-driven terminal UI for visualising and managing Docker resources like containers, images, volumes, and networks. Compose-aware and live: it reacts to Docker events on its own and streams real time resource usage, so you're observing, not polling. No GUI, no browser tab.
┌──────────────────────────────────────────────────────────────────────────────┐
│ DockSurf 12:34:56 │
├──────────────────────────────────┬───────────────────────────────────────────┤
│ Containers Images Volumes Nets │ Container: myapp-api-1 │
│ │ ───────────────────────────────────── │
│ Name Status Health Up │ Project myapp │
│ ▾ myapp 3/4 run │ Service api │
│ ├ api Up ✓ healthy 2h │ Status Up 2 hours ✓ │
│ ├ web Up — 2h │ Health healthy │
│ └ worker Exited(1)— — │ Uptime 2h Restarts 0 │
│ ▸ infra 2/2 run │ ┌ Live stats: myapp-api-1 ──────────────┐ │
│ │ │ CPU ▐███▌ 42.1% │ │
│ redis Up — 5h │ │ MEM ▐█▌ 180 MiB / 512 MiB (35%) │ │
│ │ │ NET ↓ 1.2 MB ↑ 340 KB │ │
│ │ └───────────────────────────────────────┘ │
└──────────────────────────────────┴───────────────────────────────────────────┘
Containers: 4 running / 1 stopped │ Images: 12 │ Volumes: 2 orphaned │ Context: default
q Quit r Refresh / Search ? Help l Logs s Stop u Up k Down w Disk
Docs: Quickstart · Full keybindings reference · Changelog
Highlights
- Live by default — the tables auto-refresh on
docker events(container start/stop/die, image pull/delete, …);ris a manual reload. - Docker Compose–aware — containers are grouped by project into a collapsible tree, with project-wide up / down / stop / start / restart and interleaved, colour-coded-per-service logs.
- Live resource stats — CPU %, memory, network and block I/O streamed into the detail pane for the selected running container.
- Full-control log viewer — live follow, in-log search with
n/Nmatch jumping, a timestamps toggle, configurable tail depth and--sincewindow, line wrap for long JSON, jump-to-top/bottom, and one-key export of the buffer to a file. - Operational signals at a glance — colour-coded health column, uptime and restart count, plus recent health-check probe output in the detail pane.
- Full lifecycle control — pause/unpause and kill sit alongside stop/start/restart, so a
stopthat hangs on its 10s timeout never needs the CLI. - Multi-select + bulk actions — mark rows on any tab and stop/start/remove them as a batch; ideal for cleaning up after a test run.
- Inspect & prune — the full raw
docker inspectJSON for any resource in a searchable modal, plus a one-key menu to prune stopped containers, dangling images, unused volumes/networks, or everything at once. - Image / volume / network operations — pull images with live progress, view layer history, tag, and bulk-clean dangling images; create volumes and check per-volume disk size; create networks and connect/disconnect containers, with per-container IP/MAC in the detail pane.
- Power-user exec & copy — a custom exec command with a chosen user,
docker cpin/out of a container, and an on-demanddocker topprocess snapshot, all via quick prompts. - Disk usage — a
docker system dfbreakdown (per-type size + reclaimable) on demand. - Local or remote — honours your active
docker contexton startup, so it manages the same daemon your CLI does: local, Docker Desktop, Colima, or a remote host over SSH/TCP. - In-app context switching — list and switch Docker contexts from inside the TUI (
D) without ever runningdocker context use, so other terminals keep whatever context they already had. The choice is remembered across restarts. - Auto-reconnect — if the daemon goes down mid-session, the status bar flags it immediately and DockSurf reconnects on its own the moment it's back, with a full refresh — no restart required.
Requirements
- Python 3.11+
- A reachable Docker daemon (local, or a remote one via
docker context) uv(package manager)- The
dockerCLI onPATH— only needed for exec-shell (e/E), Compose project actions (u/k), and file copy (C); everything else uses the SDK. DockSurf degrades gracefully if it's absent.
Install
See QUICKSTART.md for install + first steps in under 2 minutes.
git clone <repo>
cd docksurf
uv venv && source .venv/bin/activate
uv pip install -e .
docksurf
Or without installing:
uv run python -m docksurf_py.app
Keybindings
The essentials — full reference (per-tab keys, log pane, Compose header behaviour) lives in KEYBINDINGS.md.
| Key | Action |
|---|---|
? |
Help screen — every keybinding, in-app |
r |
Refresh all Docker data |
/ |
Search / filter the active tab |
↑/↓, Tab |
Navigate rows / switch tabs |
s/S/x |
Stop / start / restart (Containers tab) |
e |
Exec shell into the focused container |
l |
Toggle log viewer |
space |
Mark for a bulk action |
d |
Delete the selected — or marked — resource(s) |
i |
Inspect (raw docker inspect JSON) |
P |
Prune menu |
D |
Switch Docker context |
q |
Quit |
Tabs
Every tab has a leading mark column (space to toggle) for multi-select + bulk actions — see Keybindings.
Containers — all containers (running and stopped), grouped by Compose project into a collapsible tree (project header → service rows), with standalone containers listed below. Columns: name, status, colour-coded health, and uptime. The detail pane shows image, ports, networks, project/service, uptime, restart count, a collapsible environment-variable section, a collapsible recent-health-probe log, a live CPU/mem/net/block-IO panel for a running container, and — on demand (t) — a docker top running-process snapshot.
Images — all images, tagged as In Use, Unused, or Dangling. Detail pane shows size, created date, architecture, and which containers reference the image. Pull new images with live progress (+), view per-layer history (h), retag (y), and one-key mark-all-dangling for bulk cleanup (a).
Volumes — all volumes, tagged as In Use or Orphaned. Detail pane shows mountpoint, driver, labels, and attached containers. Create volumes (+) and pull on-demand per-volume size on disk (b).
Networks — all networks with driver and scope. Detail pane shows driver, scope, subnet, gateway, and each attached container's IP/MAC within the network. Create networks (+) and connect/disconnect containers (v/m).
Architecture
Eleven top-level modules/packages with strict layering (models.py and constants.py are leaf nodes and nothing imports into them). Four of the eleven — docker, renderer, actions, widgets — are packages split into one file per class/mixin, each re-exporting exactly what the equivalent single file used to expose, so nothing outside the package needed to change:
constants.py— widget/tab/table IDs, the SafeMarkup render-boundary marker, and Rich markup helpers.models.py— typed dataclasses for every resource (Container, Image, Volume, Network, ComposeProject, ContainerStats, ContainerTop, SystemDf, ContextInfo, …). No presentation logic.connection.py— Docker connection detection/classification, context and host resolution.docker/— all Docker I/O via the Docker SDK for Python. DockerClient (client.py) owns the SDK connection (honouring docker context, with an in-app override forswitch_context) and every management call — full container lifecycle (stop/start/restart/pause/kill/remove), prune, inspect,top, file copy, and context listing/switching; it also detects a dropped daemon (mark_disconnected) so reconnecting doesn't need a restart. DockerResourceFetcher (fetcher.py) does the parallel reads; LogStream/MergedLogStream/StatsStream/EventStream (streams.py) are the live iterators; context resolution (context.py) and display formatting (format.py) round out the package.service.py— the DockerService protocol DockerClient implements (swappable in tests).widgets/— Textual UI components with no Docker knowledge, one file per widget/screen: ContainerTable, DetailPane, LogPane, SearchBar, ConfirmDialog, HelpScreen, SystemDfScreen, InspectScreen, PruneScreen, PromptScreen, StatusBar (renders a connection-lost indicator).renderer/,actions/,search.py,observability.py— focused mixin classes composed into DockSurfApp in app.py, driven by a single per-tab ResourceEntry registry: table rendering, snapshot/event lifecycle, connection-state tracking, and multi-select marking (renderer/); container lifecycle, Compose, delete, bulk actions, inspect, prune, exec, file-copy, and context-switching actions (actions/); search (search.py); live stats, disk usage, anddocker top(observability.py).
How data is fetched
DockSurf talks to Docker through the SDK (docker-py), not the CLI but with three sanctioned exceptions, all guarded on the docker CLI being present: the interactive exec-shell (needs a real TTY the SDK can't hand back), Compose project actions (docker-py has no Compose support, and up must recreate containers from the compose file), and file copy (docker cp) — the SDK only exposes raw tar archives, and safely reproducing docker cp's copy semantics isn't worth it for a convenience feature.
- Snapshots — all four resource types are fetched in parallel via
ThreadPoolExecutoron each refresh, so the UI never blocks. - Event-driven refresh — a background worker subscribes to
docker eventsand debounces bursts into a refresh, ignoring high-frequency noise (exec probes, per-interval health-status pings). A refresh preserves your current selection rather than resetting the cursor. - Live stats —
container.stats(stream=True)is streamed for the selected running container on a background thread and rendered into the detail pane (one stream at a time, to stay cheap). - Logs — streamed live from the SDK's generator; timestamps are always fetched (shown on demand), and tail depth /
--sincewindow are configurable per view. A Compose project's logs are merged across its containers with a colour-coded per-service prefix.
Docker contexts — local and remote
DockSurf connects to whatever daemon your active Docker context points at (matching the docker CLI's precedence: DOCKER_HOST → active context → default socket). That doesn't have to be your local machine, and — since context switching now happens in-app — it doesn't require restarting DockSurf either.
Creating a context is still a docker CLI step (DockSurf lists and switches contexts, it doesn't create them):
# A context pointing at a remote Linux host over SSH — any host with a
# reachable Docker daemon and SSH access works: a cloud VM, a bare-metal
# box, a Raspberry Pi, a home server.
docker context create prod --docker "host=ssh://user@prod.example.com"
# Or over plain TCP, if the daemon's API is exposed that way
docker context create staging --docker "host=tcp://staging.example.com:2375"
Switching contexts from inside DockSurf — press D to list every context docker context ls knows about and pick one. This is in-app only: it never runs docker context use, so it doesn't touch ~/.docker/config.json or repoint any other terminal's docker/docker compose — DockSurf just opens its own connection to the chosen context's daemon. The choice is remembered across restarts (~/.local/share/docksurf/state.json).
Auto-reconnect — if the daemon your active context points at goes down mid-session (VM reboot, daemon restart, network blip), the status bar flags it immediately (● <reason>) and DockSurf retries on its own every couple of seconds, reconnecting and refreshing the moment it's back — no restart, no manual r.
Common use cases:
- Managing a Linux VM (cloud instance, on-prem box, home server) over SSH without keeping a shell session open
- Switching between staging and production daemons from one tool, mid-session, with
D - Inspecting a production server's containers without full shell access
- Managing a Raspberry Pi or other edge device from your laptop
Any endpoint that speaks the Docker Engine API works this way — a plain Linux daemon, Docker Desktop, Colima, Rancher Desktop, or a remote host over SSH/TCP. Managed platforms that don't expose that API can't be reached this way, even with a custom context — Azure Container Apps is the common trap: it's Kubernetes-based under the hood with no Docker Engine endpoint at all, so docker context/DockSurf/docker ps have nothing to talk to. Azure Container Instances had a (now-deprecated) docker context create aci integration; Container Apps never did — manage those via az containerapp, the Azure Portal, or the Azure SDK instead.
Logging
App logs are written to ~/.local/share/docksurf/docksurf.log — never to stdout (which belongs to the TUI). Useful for debugging refresh errors, failed Docker API calls, container/Compose action results, and stream lifecycle events.
Changelog
See CHANGELOG.md for release history.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file docksurf-0.1.0.tar.gz.
File metadata
- Download URL: docksurf-0.1.0.tar.gz
- Upload date:
- Size: 180.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7ce07359f032642c28a05a25945d4fd997beb974419456fe287eb8f8a408840
|
|
| MD5 |
961d856674cd055765e8e7fe388de269
|
|
| BLAKE2b-256 |
de790d2abd856c08ffeec8b66a52c6f470273054bef76b1ceb99570d992b54c5
|
Provenance
The following attestation bundles were made for docksurf-0.1.0.tar.gz:
Publisher:
publish.yml on praneeth-etta/docksurf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
docksurf-0.1.0.tar.gz -
Subject digest:
f7ce07359f032642c28a05a25945d4fd997beb974419456fe287eb8f8a408840 - Sigstore transparency entry: 2171963677
- Sigstore integration time:
-
Permalink:
praneeth-etta/docksurf@6a1677616473110e94dfd467b96add8560410611 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/praneeth-etta
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6a1677616473110e94dfd467b96add8560410611 -
Trigger Event:
push
-
Statement type:
File details
Details for the file docksurf-0.1.0-py3-none-any.whl.
File metadata
- Download URL: docksurf-0.1.0-py3-none-any.whl
- Upload date:
- Size: 126.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c99eded68c34dbcb00b3952e56bc122a0a7a1491d6a7e7f72c49b596d311f80
|
|
| MD5 |
ab8dfb4bcab3fce309627b987bebb1f5
|
|
| BLAKE2b-256 |
d74a88a30f0029a6f17ab7fffdc6c7ca1a60a132b95c082eeff915b67be49e6c
|
Provenance
The following attestation bundles were made for docksurf-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on praneeth-etta/docksurf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
docksurf-0.1.0-py3-none-any.whl -
Subject digest:
6c99eded68c34dbcb00b3952e56bc122a0a7a1491d6a7e7f72c49b596d311f80 - Sigstore transparency entry: 2171963690
- Sigstore integration time:
-
Permalink:
praneeth-etta/docksurf@6a1677616473110e94dfd467b96add8560410611 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/praneeth-etta
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6a1677616473110e94dfd467b96add8560410611 -
Trigger Event:
push
-
Statement type: