A lightweight zero-bloat Rust library for detecting Docker and Podman containers, mapping their published ports to host sockets, and controlling container lifecycle. Built for embedding into CLI tools and system utilities that need container awareness without pulling in a full Docker SDK.
- Container detection - Queries Docker and Podman daemons to discover running containers and their published port bindings.
- Port-to-container mapping - Resolves which container owns a given host
(ip, port, protocol)tuple, with wildcard and proxy-fallback matching. - Container lifecycle control - Stop or kill containers by ID through the daemon API (graceful SIGTERM or immediate SIGKILL).
- Multi-transport support - Connects via Unix domain sockets, Windows named
pipes, or TCP (
DOCKER_HOST), with automatic discovery of socket paths. - Rootless Podman support - Resolves rootless Podman containers on Linux by reading overlay storage metadata and matching network namespace paths.
- Background detection - Spawns detection on a background thread so callers can do other work (socket enumeration, process lookup) concurrently.
- Minimal dependencies - Only
serde,serde_json,httparse, andlogat runtime. No async runtime, notokio, nohyper. - Cross-platform - Works on Linux (x86-64) and Windows (x86-64).
Most Rust Docker libraries (bollard, docker-api) are full API clients that
require an async runtime and pull in 30-50+ transitive dependencies. nanodock
takes the opposite approach: synchronous, minimal, and focused.
| Crate | Async | Runtime Deps | Scope |
|---|---|---|---|
bollard |
Yes | ~50+ | Full Docker API |
docker-api |
Yes | ~30+ | Full Docker API |
| nanodock | No | 4 | Detection + Ports + Lifecycle |
Use nanodock when you need container awareness (detection, port mapping, lifecycle control) without pulling in an async runtime or a full Docker SDK. Ideal for CLI tools, system utilities, and monitoring agents.
Add nanodock to your Cargo.toml:
[dependencies]
nanodock = "0.1"Two detection paths are available:
Best-effort path (background thread, never errors):
use nanodock::{start_detection, await_detection};
fn main() {
// Spawn background detection (queries Docker/Podman daemon).
let handle = start_detection(None);
// ... do other work while detection runs ...
// Collect results (blocks up to 3 seconds).
let port_map = await_detection(handle);
for ((ip, port, proto), info) in &port_map {
println!(
"{proto} port {port} -> container '{}' (image: {})",
info.name, info.image
);
}
}Strict path (synchronous, returns errors):
use nanodock::detect_containers;
fn main() {
match detect_containers(None) {
Ok(port_map) => {
for ((ip, port, proto), info) in &port_map {
println!(
"{proto} port {port} -> container '{}' (image: {})",
info.name, info.image
);
}
}
Err(e) => eprintln!("detection failed: {e}"),
}
}use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use nanodock::{
start_detection, await_detection,
lookup_published_container, PublishedContainerMatch, Protocol,
};
fn main() {
let port_map = await_detection(start_detection(None));
let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5432);
match lookup_published_container(&port_map, socket, Protocol::Tcp, false) {
PublishedContainerMatch::Match(info) => {
println!("Port 5432 belongs to '{}' ({})", info.name, info.image);
}
PublishedContainerMatch::NotFound => {
println!("No container found for port 5432");
}
PublishedContainerMatch::Ambiguous => {
println!("Multiple containers match port 5432");
}
_ => {}
}
}use nanodock::{stop_container, StopOutcome};
fn main() {
let container_id = "abc123def456";
match stop_container(container_id, false, None) {
StopOutcome::Stopped => println!("Container stopped"),
StopOutcome::AlreadyStopped => println!("Container was already stopped"),
StopOutcome::NotFound => println!("Container not found"),
StopOutcome::Failed => println!("Could not reach daemon"),
_ => println!("Unexpected outcome"),
}
}nanodock communicates directly with the Docker/Podman daemon using the
/containers/json REST API endpoint over local transports:
┌─────────────┐ HTTP/1.0 GET /containers/json
│ nanodock │ ──────────────────────────────────────┐
│ (your app) │ │
└─────────────┘ ▼
┌─────────────────┐
Unix socket (/var/run/docker.sock) ───► │ │
Named pipe (\\.\pipe\docker_engine) ──► │ Docker/Podman │
TCP (DOCKER_HOST=tcp://...) ───► │ Daemon │
│ │
└─────────────────┘
DOCKER_HOSTenvironment variable - If set, the specified transport (tcp://, unix://, npipe://) is used first.- Platform-native sockets - On Linux, well-known Unix socket paths are probed (rootful Docker, rootless Docker, Podman). On Windows, named pipes for Docker Desktop and Podman Machine are tried.
- Rootless Podman overlay (Linux only) - For containers managed by rootless
Podman, nanodock reads the overlay storage metadata to resolve container
names from network namespace paths. This handles the case where
rootlessportis the process holding the socket instead of the container itself.
| Platform | Transport | Path |
|---|---|---|
| Linux | Unix socket | /var/run/docker.sock |
| Linux | Unix socket | /run/user/{uid}/docker.sock |
| Linux | Unix socket | $HOME/.docker/desktop/docker.sock |
| Linux | Unix socket | $HOME/.docker/run/docker.sock |
| Linux | Unix socket | /run/user/{uid}/podman/podman.sock |
| Linux | Unix socket | /run/podman/podman.sock |
| Windows | Named pipe | \\.\pipe\docker_engine |
| Windows | Named pipe | \\.\pipe\podman-machine-default |
| Both | TCP | DOCKER_HOST=tcp://host:port |
Full API documentation is available on docs.rs.
| Type | Description |
|---|---|
Protocol |
Network protocol enum (Tcp, Udp) |
ContainerInfo |
Container metadata (id, name, image) |
ContainerPortMap |
HashMap mapping (ip, port, protocol) to ContainerInfo |
PublishedContainerMatch |
Result of looking up a socket in the port map |
StopOutcome |
Result of a stop/kill request |
DetectionHandle |
Handle for in-progress background detection |
Error |
Error type for strict-path detection failures |
| Function | Description |
|---|---|
detect_containers(home) |
Synchronous detection, returns Result<Map, Error> |
start_detection(home) |
Spawn background daemon query, returns handle |
await_detection(handle) |
Block for results (3s timeout), returns map |
lookup_published_container() |
Match a socket against the port map |
stop_container(id, force, home) |
Stop or kill a container by ID |
parse_containers_json(body) |
Parse raw /containers/json response |
parse_containers_json_strict() |
Strict parse that returns Result on invalid JSON |
| Function | Description |
|---|---|
is_podman_rootlessport_process(name) |
Check if a process name is rootlessport |
lookup_rootless_podman_container() |
Resolve container from rootlessport PIDs |
RootlessPodmanResolver |
Cached resolver for rootless Podman |
src/
├── lib.rs — Public API, detection orchestration, port matching
├── api.rs — JSON response parsing, container name resolution
├── http.rs — Minimal HTTP/1.0 response parser (via httparse)
├── ipc.rs — OS-specific transport (Unix socket, named pipe, TCP)
└── podman.rs — Rootless Podman resolver via overlay metadata (Linux)
lib.rsowns the public API surface, detection orchestration, and port-to-container matching logic. All public types are defined here.api.rsowns JSON response parsing. It converts raw daemon responses intoContainerPortMapentries.http.rsowns HTTP protocol handling. It formats requests and parses responses usinghttparse. No Docker-specific logic lives here.ipc.rsowns OS-specific transport code. Unix sockets, Windows named pipes, TCP connections, andDOCKER_HOSTparsing all live here.podman.rsowns rootless Podman resolution. It reads overlay storage metadata and OCI runtime configs to match network namespace paths to container names.
# Debug build
cargo build
# Run tests
cargo test --lib --tests
cargo test --doc
# Compile benchmarks
cargo bench --no-run
# Run benchmarks on Linux with valgrind and gungraun-runner installed
cargo bench --bench benchmarks
# Check formatting
cargo fmt --check
# Run clippy (all+pedantic+nursery at deny level)
cargo clippy --all-targets -- -D warnings
# Build documentation
cargo doc --no-deps --open
# Dependency audit (requires cargo-deny)
cargo deny checkAll of the following must pass before merging:
| Gate | Command | Purpose |
|---|---|---|
| 1 | cargo fmt --check |
Consistent formatting |
| 2 | cargo clippy |
Zero lint warnings |
| 3 | cargo test --lib --tests && cargo test --doc |
All tests pass |
| 4 | cargo bench --no-run |
Benchmarks compile |
| 5 | cargo build |
Library compiles |
| 6 | cargo doc --no-deps |
Documentation builds |
| 7 | cargo deny check |
No vulnerable/banned deps |
nanodock ships deterministic instruction-count benchmarks via Gungraun (requires Linux + Valgrind). See CONTRIBUTING.md for setup and usage details.
Install local quality gates (runs fmt, clippy, and tests before each commit):
Windows (PowerShell):
.\scripts\install-hooks.ps1Linux / macOS:
bash scripts/install-hooks.shnanodock requires the latest stable Rust toolchain (currently 1.93+) and uses edition 2024 features.
nanodock keeps its dependency tree intentionally small:
| Crate | Purpose |
|---|---|
serde |
Container metadata serialization |
serde_json |
JSON response parsing |
httparse |
HTTP/1.x response header parsing |
log |
Debug diagnostics via log facade |
libc |
Unix-only: getuid() for socket paths |
No async runtime. No TLS. No network client libraries.
See CONTRIBUTING.md for development setup, coding standards, commit message format, and the full quality gate reference.
Licensed under the MIT License.
Copyright (c) 2026 Ehsan Khan