Rust async library for managing containerd (containers, tasks, images) over gRPC. An alternative to Docker/Bollard for container lifecycle management without a Docker daemon.
Warning
Work in progress. Public API may change.
Platform support:
| Platform | State |
|---|---|
| Linux native | Works. CI tested. |
| macOS + Colima | Works. Primary development target. |
| Windows + WSL2 | Works as Linux from inside the WSL2 distro. |
| Windows native | Not supported. connect() returns an error pointing to WSL2. |
containerd-manager requires a running containerd daemon. Docker Desktop is not required - containerd runs independently.
Colima runs a Linux VM with containerd on macOS. containerd-manager talks to that VM's containerd socket directly.
Colima's default profile is provisioned with the docker runtime: it runs a
Docker daemon inside the VM, and the containerd socket there is the one
Docker itself uses. To avoid stepping on Docker, our auto-discovery (and
this guide) uses a separate, non-default colima profile dedicated to
containerd. You can run Docker Desktop or colima start (the default
docker profile) alongside without conflict.
brew install colima
# Create a dedicated containerd profile. `--runtime` is honoured only at
# profile-creation time; recreate the profile if you need to change it.
colima start --profile containerd --runtime containerdThe socket lands at ~/.colima/containerd/containerd.sock.
colima status --profile containerd # should say: runtime: containerd
ls ~/.colima/containerd/containerd.sockctr is containerd's built-in CLI and is available inside the Colima VM.
Use colima ssh to run it:
# List all namespaces
colima ssh --profile containerd -- sudo ctr namespaces ls
# List containers in a namespace (e.g. "my-app")
colima ssh --profile containerd -- sudo ctr -n my-app containers ls
# List running tasks (processes) in a namespace
colima ssh --profile containerd -- sudo ctr -n my-app tasks lsNote: containerd isolates resources by namespace. Containers created under
"my-app"are invisible to the"default"namespace and to Docker.
If you previously did colima start --runtime containerd (without --profile)
the runtime is locked onto the default profile but our auto-discovery
ignores default. Recreate as a non-default profile:
colima stop && colima delete --force
colima start --profile containerd --runtime containerdcontainerd is typically available as a system package.
# Debian / Ubuntu
sudo apt-get install containerd
# Start the daemon
sudo systemctl start containerd
sudo systemctl enable containerdThe default socket path is /run/containerd/containerd.sock.
Run containerd inside a WSL2 Linux distribution.
# Inside your WSL2 distro (e.g. Ubuntu)
sudo apt-get install containerd
sudo containerd &The socket path inside WSL2 is /run/containerd/containerd.sock. If connecting from a Windows host, map the WSL2 path or set the CONTAINERD_SOCKET environment variable.
# Path (local development)
containerd-manager = { path = "../containerd-manager" }
# Git
containerd-manager = { git = "https://github.com/mongodb-labs/containerd-manager" }use std::time::Duration;
use containerd_manager::{ContainerId, CreateContainerOpts, ReadinessStrategy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to containerd (auto-detects a Colima socket on macOS).
let client = containerd_manager::connect(None)?.with_namespace("my-app");
let image = "docker.io/library/alpine:latest";
let id = ContainerId::new("example-1")?;
// Pull. Idempotent: skips if already present in the namespace.
client.pull_image(image).await?;
let opts = CreateContainerOpts::new()
.label("app", "example")
.host_network()
.cmd(["sleep", "300"]);
client.create_container(&id, image, opts).await?;
client.start_container(&id).await?;
// Wait until ready. ProcessRunning is the simplest strategy; use
// ImageHealthcheck for Docker-compatible health-aware waiting.
client
.wait_ready(&id, Duration::from_secs(60), ReadinessStrategy::ProcessRunning)
.await?;
let output = client.exec(&id, &["echo", "hello"]).await?;
println!("{}", output.stdout_str().trim());
// Stream logs live (use container_logs for a one-shot bulk read).
let mut follower = client.container_logs_stream(&id)?;
if let Some(Ok(entry)) = follower.recv().await {
print!("{}", String::from_utf8_lossy(&entry.data));
}
client.delete_container(&id, Duration::from_secs(30)).await?;
Ok(())
}A complete end-to-end example (MongoDB Atlas Local) is in examples/atlas_local.rs:
cargo run --example atlas_localconnect(socket_path: Option<PathBuf>) -> Result<Client>: synchronous; connect to containerd. Defaults to the"default"containerd namespace.Client: all container operations, scoped to the namespace set at construction.- Namespace:
namespace,with_namespace - Image:
pull_image,image_healthcheck - Lifecycle:
create_container,start_container,stop_container,delete_container,pause_container,unpause_container - Query:
inspect_container,list_containers,probe_health - I/O:
exec,container_logs,container_logs_stream - Port forwarding:
start_port_forward,start_managed_port_forward - Readiness:
wait_ready - Server:
server_version - Cache:
image_cache_len,clear_image_cache
- Namespace:
- Types:
Error,Result,ContainerId,TaskId,CreateContainerOpts,Mount,NetworkMode,NetworkOpts,PortForwardHandle,PortForwardOpts,ContainerInfo,TaskInfo,TaskStatus,ExecOutput,LogEntry,LogStream,LogFollower,ReadinessStrategy,HealthCheck,HealthStatus.
ContainerId is a validated newtype (containerd's identifier rules); construct with ContainerId::new("foo-1")?. Blocks shell injection and path traversal at the API boundary.
cargo run --example atlas_local # full MongoDB Atlas Local lifecycle + live log tail
cargo run --example follow_logs # docker-logs-f style demo with Ctrl-C cleanup
cargo run --example bridge_networking # bridge mode + port_binding (veth + socat)
cargo run --example atlas_local_bollard # side-by-side via Bollard (Docker API), for comparisonThree suites, gated by feature flags. See TESTING.md for the full table.
cargo test # unit tests, no setup
cargo test --features e2e-tests # needs colima running
cargo test --features e2e-colima-profiles -- --test-threads=1 --nocapture # destructiveThe containerd socket path is resolved in this order:
| Priority | Source | Example |
|---|---|---|
| 1 | Explicit argument to connect() |
connect(Some("/custom/path.sock".into())) |
| 2 | CONTAINERD_SOCKET environment variable |
export CONTAINERD_SOCKET=~/.colima/containerd/containerd.sock |
| 3 | Platform default | macOS: scans ~/.colima/<profile>/ (skips default), Linux: /run/containerd/containerd.sock |
containerd uses namespaces to isolate resources. The namespace is set on the Client and applies to all operations:
// Default namespace
let client = containerd_manager::connect(None)?;
assert_eq!(client.namespace(), "default");
// Custom namespace
let client = client.with_namespace("my-app");Different namespaces see different containers, images, and tasks. with_namespace returns a new Client that shares the underlying connection.
Error: containerd socket not found at '~/.colima/containerd/containerd.sock'
Cause: containerd is not running, or the socket is at a different path.
Fix:
- Start containerd:
- macOS:
colima start --profile containerd --runtime containerd - Linux:
sudo systemctl start containerd - WSL2:
sudo containerd &
- macOS:
- If the socket is at a non-default path, set the env var:
export CONTAINERD_SOCKET=/path/to/containerd.sock - Verify the socket exists:
ls -la /path/to/containerd.sock
Error: connection refused to containerd socket at '...'
Cause: The socket file exists but nothing is listening (e.g. containerd crashed or was stopped uncleanly).
Fix:
- Check if containerd is running:
- macOS:
colima status --profile containerd - Linux:
systemctl status containerd
- macOS:
- Restart it:
- macOS:
colima stop --profile containerd && colima start --profile containerd - Linux:
sudo systemctl restart containerd
- macOS:
- If the socket is stale, remove and restart:
sudo rm /path/to/containerd.sock # Then start containerd again
Error: port forwarding error: ...
Cause: The host port requested for start_port_forward is already bound by another process.
Fix:
- Find what's using the port:
lsof -i :27017 # macOS / Linux - Stop the conflicting process, or choose a different host port.
- If a previous run left a stale forward, ensure you dropped the
PortForwardHandleor deleted the container.
FATA: error starting vm: error at 'starting': exit status 1
Cause: Stale VM state from a previous unclean shutdown, or the disk was provisioned for a different runtime.
Fix:
# Delete the existing instance and start fresh
colima delete --profile containerd --force
colima start --profile containerd --runtime containerd
# If the error mentions "runtime disk provisioned for docker runtime":
colima delete --profile containerd --data
colima start --profile containerd --runtime containerdCause: Large images (like MongoDB Atlas Local) take time on first pull. Failures are usually network-related.
Fix:
- Verify network connectivity inside the VM:
- macOS:
colima ssh -- ping -c1 registry-1.docker.io
- macOS:
- Check if the image reference is correct (include the registry prefix):
docker.io/mongodb/mongodb-atlas-local:latest # correct mongodb/mongodb-atlas-local:latest # may not resolve - Subsequent pulls of the same image are fast (content is cached in the store).
rust-version = "1.88"
Apache-2.0