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
- 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.
- Operational signals at a glance — colour-coded health column, uptime and restart count, plus recent health-check probe output in the detail pane.
- Disk usage — a
docker system dfbreakdown (per-type size + reclaimable) on demand. - Local or remote — honours your active
docker context, so it manages the same daemon your CLI does like the local, Docker Desktop, Colima, or a remote host over SSH.
- 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) and Compose project actions (u/k); everything else uses the SDK. DockSurf degrades gracefully if it's absent.
git clone <repo>
cd docksurf-py
uv venv && source .venv/bin/activate
uv pip install -e .
docksurf-pyOr without installing:
uv run python -m docksurf_py.app| Key | Action |
|---|---|
r |
Refresh all Docker data |
/ |
Open search / filter for active tab |
w |
Disk-usage screen (docker system df) |
? |
Help screen |
q |
Quit |
d |
Delete selected resource (with confirmation) |
Tab |
Switch between tabs |
↑/↓ |
Navigate rows |
The container/project keys are context-sensitive: on a Compose project header (compose stack) row they act on the whole project; on a single container row they act on that container.
| Key | On a container row | On a project header row |
|---|---|---|
s |
Stop container | Stop whole project |
S |
Start container | Start whole project |
x |
Restart container | Restart whole project |
l |
Toggle log viewer | Aggregated project logs |
e |
Exec shell (bash→sh) |
— |
u |
Compose up (docker compose up -d) — brings the focused project up |
|
k |
Compose down (docker compose down, confirmed) — tears the project down |
|
space |
Collapse / expand the focused Compose project group |
| Key | Action |
|---|---|
f |
Toggle live log follow |
z |
Expand log pane to full width / collapse |
l |
Close log viewer, return to detail pane |
/ |
Filter log lines (Esc to clear) |
The ⛶ Expand / ⊡ Collapse button in the log toolbar is also clickable.
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, and — for a running container — a live CPU/mem/net/block-IO panel.
Images — all images, tagged as In Use, Unused, or Dangling. Detail pane shows size, created date, architecture, and which containers reference the image.
Volumes — all volumes, tagged as In Use or Orphaned. Detail pane shows mountpoint, driver, labels, and attached containers.
Networks — all networks with driver and scope. Detail pane shows subnet, gateway, and attached containers.
Eleven modules with strict layering (models.py and constants.py are leaf nodes and nothing imports into them):
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, SystemDf, …). No presentation logic.connection.py— Docker connection detection/classification, context and host resolution.docker.py— all Docker I/O via the Docker SDK for Python. DockerClient owns the SDK connection (honouring docker context); DockerResourceFetcher does the parallel reads; LogStream/MergedLogStream/StatsStream/EventStream are the live iterators.service.py— the DockerService protocol DockerClient implements (swappable in tests).widgets.py— Textual UI components with no Docker knowledge: ContainerTable, DetailPane, LogPane, SearchBar, ConfirmDialog, HelpScreen, SystemDfScreen, StatusBar.renderer.py,actions.py,search.py,observability.py— focused mixin classes (table rendering & snapshot/event lifecycle, container/Compose/delete actions, search, and live stats + disk usage) composed into DockSurfApp in app.py, driven by a single per-tab ResourceEntry registry.
DockSurf talks to Docker through the SDK (docker-py), not the CLI but with two sanctioned exceptions, both guarded on the docker CLI being present: the interactive exec-shell (needs a real TTY the SDK can't hand back) and Compose project actions (docker-py has no Compose support, and up must recreate containers from the compose file).
- 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; a Compose project's logs are merged across its containers with a colour-coded per-service prefix.
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.
# Create a context pointing at a remote server over SSH
docker context create prod --docker "host=ssh://user@prod.example.com"
# Switch to it
docker context use prod
# DockSurf now shows that server's containers, images, volumes, and networks
docksurf-pyThe active context name and endpoint are shown in the status bar at the bottom of the TUI. Switch contexts between runs to manage different environments from the same tool.
Common use cases:
- Managing a VPS or cloud instance without keeping an SSH session open
- Inspecting a production server's containers without full shell access
- Managing a Raspberry Pi or edge device from your laptop
Any runtime that registers a Docker context works — Docker Desktop, Colima, Rancher Desktop, or a plain daemon on a remote host.
App logs are written to ~/.local/share/docksurf-py/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.