Building Docsee — a cross-platform Docker management tool with a Tauri GUI and terminal TUI, and why Rust was the right choice.
I manage a lot of Docker containers. At Prachyam, the local-infra stack ran 30+ containers across profiles — Mailcow, Nextcloud, Tailscale, PostgreSQL, Redis, monitoring, the works. Docker Desktop was the tool I reached for first: a full GUI, container inspection, log tailing, resource usage. It's an Electron app sitting at ~500 MB of memory before you've opened a single container view.
On an M1 Max with 64 GB, that's survivable. On the iMac I was actually coding on at Prachyam — 16 GB shared across the monorepo build, Expo bundler, and Docker service stack — Docker Desktop was the wrong tool. It was taking memory I needed for actual work.
docker ps in the terminal is fast but gets old when you're checking container health 20 times a session. I wanted something fast, lightweight, native, and keyboard-driven. So I built Docsee.
Three reasons:
Docsee has two faces:
docsee/
├── docsee-core/ # Shared Rust library
│ ├── docker.rs # Bollard client wrapper
│ ├── container.rs # Container operations
│ ├── image.rs # Image management
│ └── system.rs # System info, disk usage
├── docsee-tui/ # Terminal UI (Ratatui)
│ ├── app.rs # TUI app state
│ ├── ui.rs # Layout and rendering
│ └── handler.rs # Key event handling
├── docsee-gui/ # Desktop GUI (Tauri + Svelte)
│ ├── src-tauri/ # Rust backend
│ └── src/ # Svelte frontendThe docsee-core crate is the shared brain. Both the TUI and GUI import it. This means container operations, error handling, and Docker API communication are written once.
Rust doesn't have an official Docker SDK, but Bollard is excellent. It's async, well-typed, and covers the full Docker API:
use bollard::Docker;
use bollard::container::ListContainersOptions;
pub async fn list_containers(docker: &Docker) -> Result<Vec<ContainerInfo>> {
let options = ListContainersOptions::<String> {
all: true,
..Default::default()
};
let containers = docker.list_containers(Some(options)).await?;
Ok(containers.into_iter().map(|c| ContainerInfo {
id: c.id.unwrap_or_default(),
name: c.names.unwrap_or_default().first()
.map(|n| n.trim_start_matches('/').to_string())
.unwrap_or_default(),
state: c.state.unwrap_or_default(),
status: c.status.unwrap_or_default(),
image: c.image.unwrap_or_default(),
}).collect())
}Ratatui makes terminal UIs surprisingly pleasant to build. The main loop is a standard event-driven pattern:
loop {
terminal.draw(|frame| ui::render(frame, &app))?;
if event::poll(Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) => match key.code {
KeyCode::Char('q') => break,
KeyCode::Up => app.previous(),
KeyCode::Down => app.next(),
KeyCode::Enter => app.toggle_container().await?,
KeyCode::Char('d') => app.delete_container().await?,
KeyCode::Char('l') => app.view_logs().await?,
_ => {}
},
_ => {}
}
}
}The result is a snappy, keyboard-driven interface that shows container status, logs, stats, and lets you start/stop/delete with single keystrokes.
The sub-50ms target? Here are the real numbers:
| Operation | Time | |-----------|------| | List containers | ~12ms | | Start container | ~35ms | | Stop container | ~28ms | | Container logs (last 100 lines) | ~18ms | | System info | ~8ms |
These are measured from command to display update. Rust's zero-cost abstractions and Bollard's efficient HTTP handling make this possible.
Rust's ownership model is a teacher. It forced me to think about data lifetimes in ways that made me a better programmer in every other language too.
Tauri is production-ready. If you're building a desktop app in 2025 and reaching for Electron, reconsider. Tauri gives you native performance with web frontend flexibility.
Build the tool you need. Docsee started as a weekend project. It's now something I use daily. The best way to learn a language is to build something you'll actually use.
Docsee is open source on GitHub. PRs welcome.
Karanveer Singh Shaktawat
Full Stack Engineer & Infrastructure Architect
I build production systems across web, mobile, and infrastructure — then document what went wrong and why.
Pick what you want to hear about — I'll only email when it's worth it.
Did this resonate?
How to use Docker multi-stage builds to go from a 2GB Rust build image to a 12MB production image.
The difference between static dispatch (impl Trait) and dynamic dispatch (dyn Trait) — not just syntax, but a real tradeoff.
One changed line in a Dockerfile invalidates every layer after it. Ordering your Dockerfile with this in mind cuts rebuild times dramatically.