# Ducker: a Rust Docker TUI where exec needs bash and the network delete modal does nothing

> A terminal UI for Docker containers, images, volumes and networks by Robert Soane, packaged through cargo, pacman, Homebrew and nixpkgs. The interesting parts are the pages and hotkeys model, the two deletion bugs the documentation admits to, and a release profile tuned for binary size rather than speed.

**robertpsoane/ducker** — A slightly quackers Docker TUI based on k9s 🦆

- Repository: https://github.com/robertpsoane/ducker
- Website: https://ducker.soane.io
- Stars: 932 · Forks: 27
- Language: Rust
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/robertpsoane-ducker

## Install paths, and a warning about the one that will break

There is no downloadable binary. The primary install path builds from source, and the documentation attaches a warning to it that is worth repeating before you type anything:

```bash
cargo install --locked ducker
```

Without `--locked`, upstream dependency versions can move and break the build. That warning is the first thing a new user sees, and it is unusual to see a package ask for a locked install by default rather than as a troubleshooting note.

The toolchain floor is Rust 1.88, which matches the `rust-version` field in Cargo.toml and the edition 2024 declaration. Two Rust versions are named in the README and the manifest, and they agree, which is not something you can say about every package.

Packaged installs exist for three distributions. Arch Linux serves it from the official extra repository:

```sh
pacman -S ducker
brew install ducker
nix-shell -p ducker
```

On Nix you can skip installing entirely with `nix run nixpkgs#ducker`, and a flakes version of the same thing is given as the alternative. Persistent installation means adding it to `environment.systemPackages`. There is also an install.sh at the repository root, which is not one of the documented paths.

For the bleeding edge the route is a git install, `cargo install --git https://github.com/robertpsoane/ducker`, which builds whatever the default branch holds rather than what crates.io holds.

## The crate version and the branch have drifted apart

Cargo.toml declares version 0.6.5, and v0.6.5 is the newest tag, published on 21 March 2026, after v0.6.4 in March and v0.6.3 in February. The default branch was last pushed on 3 August 2026.

So there are roughly five months of commits on master that no released crate contains. Nothing in the visible material marks those commits as unstable in a way you can see from the outside; you only find out by comparing dates.

The release profile explains part of why the published binary is what it is. The release profile strips symbols, enables link time optimization, sets codegen units to 1, sets panic strategy to abort, and sets `opt-level = "z"`, which optimizes for size rather than for speed. For a terminal UI that renders text, size wins and nobody will notice.

Two details in Cargo.toml are worth knowing about if you ever vendor this. The published package includes only `/src`, the license, the changelog and the README, so the docs directory, the install script, the demo animation and the devcontainer configuration do not travel to crate consumers. And the ratatui family is grouped under a comment saying these often need to be updated in lockstep: ratatui itself at 0.30.0, ratatui-macros at 0.7.0, ansi-to-tui at 8.0.1 and tui-tree-widget at 0.24.0.

The Docker API side is bollard at 0.20.1 with the ssl feature enabled, so TLS to a remote daemon is in scope. Sizes are rendered through byte-unit, config paths through dirs-next, and the YAML config is read with serde_yml rather than serde_yaml, which is a separate crate.

## Four pages, one prompt, and two different legends

The interface is organised into pages, and the model is worth understanding before you press anything. Top-level pages are reached by typing commands at a prompt rather than by hotkey, while actions inside a page are hotkey driven. Two legends are on screen at once: the global hotkeys sit along the bottom edge, and the page-specific hotkeys sit in the top right corner, and which ones appear depends on where you are.

The page commands are `images`, `containers`, `volumes`, `networks`, `help` and `quit`, each with a singular alias so `image` and `container` work. The colon key opens the prompt. The global navigation keys are j and k for up and down, g for the top of a list and G for the bottom, and Q or q to close.

That gives four resource pages and a help page, and nothing else. There is no compose or stack page, no registry page, no build page and no Kubernetes resource view. The inspiration credit goes to k9s, which is a Kubernetes UI, so the resemblance is in the layout and key conventions rather than in feature coverage. Anyone arriving from k9s expecting a stack editor will not find one.

Sorting is done with Shift plus a letter, per page, and pressing the same key twice reverses the direction. Containers sort by name, image, status, created and ports; images by name, created, tag and size; networks by name, created, scope and driver; volumes by name, created, driver and mountpoint.

## The same letter means different things on different pages

Hotkeys are page-scoped, and the collisions are the part that needs a moment of attention. The letter d describes the selected object on the images, volumes and networks pages. Ctrl+d deletes it. Alt+d toggles a dangling filter on images and volumes only. On the containers page d is not bound at all, where the actions are a for exec, l for logs, r for run, s for stop and Ctrl+d for delete.

So Alt+d and Ctrl+d sit one modifier apart and mean filter and destroy respectively, which is a wide gap for keys that are physically adjacent. On a page with a dangling filter active, what you have selected may be an image that is about to disappear from view, and the filter state itself is not a selection.

Container actions are otherwise thin: run starts the selected container, stop stops it, l shows logs, and Esc returns from the logs page to the containers page rather than closing anything. There is no restart, no pause, no rename, no stats, no port mapping editor and no container creation from this interface.

The logs page is worth a note. Streaming container output into a TUI is where the awkward interactions live, and the only documented action there is Esc to go back.

## Exec needs bash, and the network delete modal is a no-op

Two gaps are documented rather than hidden, and both are in the destructive path.

The first is exec. Pressing a on a container opens a shell inside it, but only for containers that have bash installed, and the note says the intention is to add a user option for other cases. In practice that excludes Alpine images and most distroless images, which is a large share of what a Docker user actually runs. The same gap applies to the shell you get once inside: it is bash, not sh.

The second is network deletion. The warning attached to the networks section says network deletion is not entirely complete: a failed deletion results in a yes/no modal telling you it could not be deleted, and there is no difference between the yes and no results. The stated reason is the current modal story and a quick and dirty hack to get modals set up, with a promise to patch it once a generic modal exists.

Read that precisely. It is about the failure path rather than the success path: a successful deletion works, and the broken case is the modal that appears when deletion fails and then does nothing useful either way. Still, a confirmation control whose two answers behave identically is the kind of thing you should know about before you trust it to protect you.

Images and volumes have the same delete key and, as far as the visible documentation goes, no such caveat, which makes the network path the one to be careful with.

## Config lives in a YAML file in the platform config directory

Configuration is a YAML file in whatever directory the host platform uses for user configuration, with dirs-next resolving the location per OS. The documentation for that section is cut short in what is available here, so the concrete settings it exposes cannot be confirmed from this material, and it is fair to say the configuration surface is documented lightly.

What can be said is where the file lives and how the process is wired. Settings are parsed at startup rather than watched, which follows from the way the rest of the application is built: a page-driven TUI with a prompt and hotkeys, where nothing suggests a live reload. Logging goes through tracing and tracing-subscriber with an env-filter feature, and errors are handled through color-eyre, so RUST_LOG style filtering is the likely lever and the env-filter feature is there for it.

The repository root gives a sense of what is not shipped. Alongside the manifest and lock file there is a devcontainer configuration, a VS Code directory, a docs directory, a changelog, a contributing guide, an ISSUES.md, an install.sh, an assets directory and a demo animation. The default branch is master rather than main.

The changelog is the file to read first if you are upgrading across a minor version, since the three most recent releases are close together in time and each is a small increment rather than a feature wave.

## What the dependency list says about the shape of the program

The dependency set is small and legible, which is a good sign for a TUI that has to start fast and stay out of the way.

tokio is declared with rt-multi-thread, macros and process features, and the process feature matters because exec into a container means spawning a process and wiring its streams. crossterm comes with the event-stream feature, which is how key events arrive continuously rather than one read at a time. futures and itertools are present for async plumbing and iterator work.

Two small dependencies say interesting things. dyn-clone exists to clone trait objects, which is what a page abstraction over heterogeneous Docker objects needs. uuid with the v4 feature is there for job identity, and if the application tracks jobs then the queue has identifiers you can correlate across restarts.

For an application that talks to a Docker daemon over a socket and renders tables, there is no HTTP framework, no database and no template engine in the list. State comes from the daemon. That is a coherent design: the daemon is the database, the TUI is a view, and the only local state is settings plus whatever the daemon itself remembers.

The homepage for the documentation is separate from the repository at ducker.soane.io, so the docs directory in the tree is not necessarily what the published documentation contains.

## Conclusion

Ducker suits someone who lives in the command line, wants Docker state in one scrollable view and is comfortable with a tool that exposes only the four Docker objects it was built around. It does not suit Kubernetes work, compose stacks or registries, because those are not pages here. Before you rely on it, confirm the crate version you get matches what you need, since Cargo.toml still says 0.6.5 from March while the branch has moved since, and remember that exec will not work on a distroless or Alpine image. On images, volumes and networks, treat the delete key as the dangerous one and re-read the confirmation, because the network path currently reports success regardless of what you choose.

## FAQ

### How do I install ducker?

There is no prebuilt binary, so use cargo install --locked ducker with cargo 1.88 or higher. Arch Linux users can run pacman -S ducker, Homebrew users can run brew install ducker, and Nix users can try nix-shell -p ducker or add it to environment.systemPackages.

### How do I use ducker in the terminal?

Type page commands at the colon prompt: images, containers, volumes, networks, help and quit, each with a singular alias. Inside a page, j and k move, g and G jump to the top and bottom, and Shift plus a letter sorts columns per page.

### Why does exec into a container not work on my ducker container?

Exec currently only supports containers with bash installed, so Alpine and distroless images will not work. The documentation notes the intention to add a user option for this, and until then you have to use a shell from inside the image instead.

### Is ducker related to Kubernetes like k9s?

Only in inspiration. Ducker is a Docker terminal UI with pages for containers, images, volumes and networks. There is no compose, stack, registry or Kubernetes view, and the README credits k9s for the layout and key conventions.

### Where is ducker configuration stored?

In a YAML file in the host platform's user configuration directory, located per operating system. Logging runs through tracing-subscriber with env-filter, and errors are handled through color-eyre.

## Sources

- [License: MIT](https://github.com/robertpsoane/ducker/blob/master/LICENSE)
- [Project website](https://ducker.soane.io)
- [README](https://github.com/robertpsoane/ducker/blob/master/README.md)
- [Releases](https://github.com/robertpsoane/ducker/releases)
- [robertpsoane/ducker on GitHub](https://github.com/robertpsoane/ducker)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/robertpsoane-ducker
