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, we run Mailcow, NextCloud, Tailscale, and various microservices — all containerized. Docker Desktop is fine, but it's an Electron app that eats RAM for breakfast. And docker ps in the terminal gets old fast when you're juggling 30+ containers.
I wanted something fast, lightweight, and native. 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:
Karanveer Singh Shaktawat
Full Stack Engineer & Infrastructure Architect
Building portfolio, contributing to open source, and seeking remote full-time roles with significant technical ownership.
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.
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.