| |

Docker 70 🐳 containerd CLI Integration: Interacting directly via ctr and nerdctl

Docker Engine has always been a layered system. When you run docker run, the CLI talks to dockerd, which delegates to containerd, which in turn uses runc to create the actual container process . The containerd layer is the core runtime: it manages images, snapshots, and container lifecycle, and it is the component that Kubernetes interacts with through the Container Runtime Interface. When you need to interact with containerd directly—without Docker in the path—you have two CLI options with very different personalities. ctr is the low-level debugging tool that ships with containerd itself. nerdctl is a Docker-compatible CLI designed for everyday use, with support for Compose, rootless mode, and image signing .

The choice between them is not about capability but about intent. ctr exposes the raw containerd API surface: namespaces, content stores, snapshots, and tasks. It is unsandboxed, not user-friendly, and explicitly documented as a debugging tool rather than a production interface . nerdctl wraps the same runtime with Docker-like commands, adds BuildKit integration for builds, and supports nerdctl compose up for multi-container applications . For a homelab or a development machine that has moved to containerd without Docker, nerdctl is the practical daily driver. For debugging why a container will not start or inspecting the content store, ctr is the tool that tells you what is actually happening inside the runtime.

Key point: ctr is the low-level debugging CLI bundled with containerd; nerdctl is a Docker-compatible CLI that provides a familiar workflow for everyday container operations, Compose support, and BuildKit builds, while still operating directly on containerd without Docker Engine.


Why both tools exist

The runtime layer problem. Docker Engine is a complete platform: it includes the CLI, the daemon, the image builder, volume management, and networking. containerd is the runtime component underneath it. When Docker was removed from Kubernetes as the default runtime (the dockershim removal in Kubernetes 1.24), the industry standardized on containerd as the CRI-compatible runtime . This means that on many systems today, containerd is running but Docker is not. The question becomes: how do you interact with the runtime when the Docker CLI is not installed?

The debugging versus daily-use distinction. ctr is shipped with containerd and is explicitly described as an “unsupported debug and administrative client” . It uses containerd‘s native namespaces, which are separate from Kubernetes namespaces and from each other. A container started with ctr run without specifying a namespace goes into the default namespace, and ctr ps in a different namespace will not see it. This is intentional: ctr is for operators who need to inspect the runtime’s internal state, not for developers who want to run a Redis container. nerdctl solves the daily-use problem by providing the Docker command surface (run, ps, exec, logs, build, compose) on top of the same containerd runtime .

The feature gap. nerdctl supports features that ctr lacks: port publishing (-p), restart policies, bridge networking, credential helpers from ~/.docker/config.json, logs, build, and compose up . These are the commands developers reach for every day. ctr has run and images pull, but it does not have -p for port mapping, and its container listing is namespaced in a way that confuses users expecting Docker behavior. The nerdctl documentation explicitly notes that ctr lacks equivalents for nerdctl run -p, nerdctl logs, nerdctl build, and nerdctl compose up .

The rootless and advanced features. nerdctl supports rootless containerd through containerd-rootless-setuptool.sh, lazy image pulling with Stargz and Nydus, encrypted images with ocicrypt, and P2P image distribution over IPFS . These are features that ctr does not expose at the same level of usability. For a homelab or a development environment that wants to experiment with these capabilities without Docker, nerdctl is the entry point.

The Kubernetes debugging angle. nerdctl --namespace k8s.io ps -a lists the containers that Kubernetes is running, because all Kubernetes containers are in the k8s.io containerd namespace . This gives you a Docker-like view of what the kubelet is doing without needing crictl, which is a separate CRI-specific tool. crictl is designed for CRI debugging and is maintained by the Kubernetes community, but it is not Docker-compatible and is focused on inspection rather than creation .


a. ctr: the low-level debugging client

ctr is installed automatically when you install containerd from the official release tarball. The binary is in the bin/ directory alongside containerd and the containerd-shim binaries . It connects to the containerd daemon over the Unix socket at /run/containerd/containerd.sock by default.

The first thing to understand about ctr is namespaces. Every operation happens within a namespace, and the default is default. If you start a container with ctr run and then list containers with ctr containers list, you are looking at the default namespace. Kubernetes containers live in the k8s.io namespace. You specify the namespace with --namespace or -n . This is a common source of confusion: a container that is running perfectly well is invisible if you are looking in the wrong namespace.

Pulling and running an image with ctr requires the full image reference, including the registry:

ctr images pull docker.io/library/redis:alpine
ctr run docker.io/library/redis:alpine redis

The ctr run command starts the container in the foreground and attaches to it. There is no -d flag in the Docker sense; ctr is not designed for detached container management. There is no port publishing. There is no restart policy. These are not oversights; they are reflections of ctr‘s purpose as a debugging tool .

Where ctr is useful is in inspecting the runtime’s internal state. ctr containers info <id> returns the full container specification. ctr tasks list shows the running tasks (a task is the runtime’s representation of a running container). ctr images list shows the images known to the runtime, and ctr content list shows the underlying content store. If a container fails to start, ctr events streams the runtime’s events, and ctr tasks info shows the task’s status and exit code. These commands are not user-friendly, but they are direct windows into what containerd is actually doing .


b. nerdctl: the Docker-compatible CLI

nerdctl is a separate binary that you install from its GitHub releases page. The nerdctl-full tarball includes the dependencies that nerdctl needs: CNI plugins for networking, BuildKit for building images, and RootlessKit for rootless mode . If you install only the nerdctl binary, you must provide those dependencies yourself.

The command surface is intentionally Docker-like:

nerdctl run -d --name web -p 8080:80 nginx:alpine
nerdctl ps
nerdctl logs web
nerdctl exec -it web sh

These commands behave as you expect from Docker. The -p flag publishes a port. The -d flag detaches. The ps command lists running containers, and logs streams the container’s stdout and stderr. This is the daily-use experience that ctr lacks .

nerdctl build uses BuildKit, which must be running as a separate daemon (buildkitd). If BuildKit is not installed and running, nerdctl build fails. This is a difference from Docker, where the builder is integrated into the daemon. The nerdctl-full tarball includes BuildKit, and the documentation recommends BuildKit v0.11.0 or later for full feature support .

nerdctl compose supports the Compose specification. You can run nerdctl compose up with a docker-compose.yaml file, and nerdctl will create the services, networks, and volumes defined in the file . This is the capability that makes nerdctl a viable replacement for Docker Compose in a containerd-only environment.

The Kubernetes integration is a practical feature for anyone debugging a cluster. nerdctl --namespace k8s.io ps -a lists the containers running in the Kubernetes namespace, with names that include the pod and container identifiers. nerdctl --namespace k8s.io logs <container-id> reads the logs of a Kubernetes container without needing kubectl or crictl . This is useful when you are on a node and want to see what a pod is doing without the API server.

Rootless mode is another differentiator. nerdctl can run containers as an unprivileged user when containerd is configured for rootless operation. The setup is containerd-rootless-setuptool.sh install, and after that, nerdctl run works without sudo . This is a security improvement for development environments and a capability that ctr does not match in usability.


c. Choosing between ctr, nerdctl, and the Docker CLI

The decision depends on what is installed and what you are trying to do. If Docker is installed and working, the Docker CLI is the right tool for development. It has the most complete feature set, the best documentation, and the broadest ecosystem support. The fact that Docker uses containerd underneath is an implementation detail that you do not need to manage.

If Docker is not installed and only containerd is present—which is common on Kubernetes nodes and on systems that have adopted the CRI-native runtime—then nerdctl is the practical daily driver. It provides the commands you expect, supports Compose, and integrates with BuildKit for builds. It is a non-core sub-project of containerd, maintained by the same community, and it is designed specifically to fill the gap that ctr leaves .

ctr remains the debugging tool. When nerdctl reports an error that you do not understand, ctr can show you the underlying container spec, the task state, and the runtime events. It is also the tool that is always present with containerd, so it is the one you can rely on when nothing else is installed. The limitation is that it is not designed for creating and managing application containers in the way developers expect.

crictl is the fourth tool in this space, and it is specifically for CRI debugging. It talks to any CRI-compatible runtime (containerd, CRI-O) and is maintained by the Kubernetes community. It is useful for inspecting pods and containers on a Kubernetes node, but it is not Docker-compatible and it does not support creating containers outside the kubelet’s control . For Kubernetes debugging, crictl is the standard; for general container work on a containerd host, nerdctl is the standard.


Complete Example Session

The following session demonstrates pulling and running a container with ctr, then the same operation with nerdctl, and shows the Kubernetes namespace listing.

# ============================================
# PART 1: PULL AND RUN WITH CTR
# ============================================
# Pull an image into the default namespace
sudo ctr images pull docker.io/library/nginx:alpine

# List images in the default namespace
sudo ctr images list

# Run a container (foreground, no port publishing)
sudo ctr run docker.io/library/nginx:alpine my-nginx

# In another terminal, list containers
sudo ctr containers list
# Output shows the container ID and image

# List running tasks
sudo ctr tasks list
# Output shows the task and its state

# ============================================
# PART 2: INSPECT WITH CTR
# ============================================
# Get the container ID from the previous list
sudo ctr containers info <container-id>

# Check the task status
sudo ctr tasks info <container-id>

# ============================================
# PART 3: PULL AND RUN WITH NERDCTL
# ============================================
# Pull the image (default namespace)
nerdctl pull nginx:alpine

# Run a detached container with port publishing
nerdctl run -d --name web -p 8080:80 nginx:alpine

# List containers
nerdctl ps

# View logs
nerdctl logs web

# Execute a command inside the container
nerdctl exec -it web sh
# Inside the container: exit

# ============================================
# PART 4: COMPOSE WITH NERDCTL
# ============================================
# Create a compose file
cat > docker-compose.yaml << 'EOF'
services:
  web:
    image: nginx:alpine
    ports:
      - "8081:80"
  redis:
    image: redis:alpine
EOF

# Start the stack
nerdctl compose -f docker-compose.yaml up -d

# List the stack containers
nerdctl compose -f docker-compose.yaml ps

# Stop the stack
nerdctl compose -f docker-compose.yaml down

# ============================================
# PART 5: KUBERNETES NAMESPACE
# ============================================
# On a Kubernetes node, list containers in the k8s.io namespace
sudo nerdctl --namespace k8s.io ps -a

# The output includes Kubernetes pods and their container IDs
# Read logs from a Kubernetes container
sudo nerdctl --namespace k8s.io logs <container-id>

# ============================================
# PART 6: CLEANUP
# ============================================
# Stop and remove the nerdctl container
nerdctl stop web
nerdctl rm web

# Remove the ctr container (find its ID first)
sudo ctr tasks kill <container-id>
sudo ctr containers remove <container-id>

# Remove the pulled images
nerdctl rmi nginx:alpine
sudo ctr images remove docker.io/library/nginx:alpine

The six parts cover ctr pull and run, ctr inspection, nerdctl run with port publishing and logs, nerdctl compose, the Kubernetes namespace listing, and cleanup. What the session demonstrates is that ctr gives you direct access to the runtime’s internal state, while nerdctl gives you the Docker workflow on top of the same runtime.


Quick Reference

Tool Comparison

CapabilityctrnerdctlDocker CLI
Runtimecontainerdcontainerdcontainerd via dockerd
Docker-compatibleNoYesYes
Port publishing (-p)NoYesYes
Detached mode (-d)NoYesYes
LogsNoYesYes
BuildNoYes (BuildKit)Yes
ComposeNoYes (nerdctl compose)Yes
RootlessLimitedYesYes
Primary purposeDebuggingDaily useDevelopment

Common ctr Commands

CommandPurpose
ctr images pull <ref>Pull an image
ctr images listList images in namespace
ctr containers listList containers
ctr containers info <id>Inspect a container
ctr tasks listList running tasks
ctr tasks info <id>Inspect a task
ctr run <ref> <name>Run a container (foreground)

Common nerdctl Commands

CommandPurpose
nerdctl run -d -p 8080:80 nginxRun detached with port
nerdctl ps -aList all containers
nerdctl logs <name>View logs
nerdctl exec -it <name> shShell into container
nerdctl build -t myimage .Build with BuildKit
nerdctl compose up -dStart Compose stack
nerdctl --namespace k8s.io ps -aList Kubernetes containers

Namespace Behavior

ToolDefault NamespaceKubernetes Namespace
ctrdefaultk8s.io
nerdctldefaultk8s.io
DockerN/AN/A

Best Practices

✅ Do This:

sudo ctr images pull docker.io/library/nginx:alpine          # ✅ full reference
sudo nerdctl run -d -p 8080:80 --name web nginx:alpine       # ✅ Docker-like
sudo nerdctl --namespace k8s.io ps -a                        # ✅ debug K8s
nerdctl compose -f stack.yaml up -d                          # ✅ Compose stack

❌ Don’t Do This:

ctr run nginx                                                # ❌ no full ref
nerdctl run -p 8080:80 nginx                                 # ❌ without sudo if rootful
# expect ctr -p port mapping                               # ❌ not supported
# confuse ctr namespaces with Kubernetes namespaces        # ❌ different concepts

Common Pitfalls

PitfallWhy It HappensFix
ctr container invisibleWrong namespaceUse --namespace flag
ctr run no port accessctr lacks -pUse nerdctl or Docker
nerdctl build failsBuildKit not runningStart buildkitd or use full tarball
Kubernetes containers not listedWrong namespacenerdctl --namespace k8s.io ps -a
nerdctl requires sudoRootful containerdUse rootless setup or sudo

Real-World Examples

1. Pull with ctr

sudo ctr images pull docker.io/library/redis:alpine

2. Run with nerdctl

nerdctl run -d --name redis -p 6379:6379 redis:alpine

3. List containers with nerdctl

nerdctl ps -a

4. View logs with nerdctl

nerdctl logs redis

5. Compose stack with nerdctl

nerdctl compose -f docker-compose.yaml up -d

6. List Kubernetes containers

sudo nerdctl --namespace k8s.io ps -a

7. Inspect container with ctr

sudo ctr containers info <id>

8. Check task state with ctr

sudo ctr tasks list

9. Build with nerdctl

nerdctl build -t myapp:v1 .

10. Rootless run

nerdctl run -d -p 8080:80 nginx:alpine

Visual

The Runtime Stack

┌──────────────────────────────────────────────┐
│  DOCKER CLI                                  │
│         │                                    │
│         ▼                                    │
│  dockerd (Docker daemon)                     │
│         │                                    │
│         ▼                                    │
│  containerd ──────────────────────────────┐  │
│         │                                 │  │
│         ├── ctr (debug CLI)               │  │
│         ├── nerdctl (Docker-like CLI)     │  │
│         │                                 │  │
│         ▼                                 │  │
│  runc (creates container process)         │  │
└──────────────────────────────────────────────┘

ctr vs nerdctl Workflow

┌──────────────────────────────────────────────┐
│  CTR                                         │
│  ctr images pull docker.io/library/nginx    │
│  ctr run docker.io/library/nginx my-nginx   │
│  ctr containers list                        │
│  (foreground, no ports, debugging)          │
│                                              │
│  NERDCTL                                     │
│  nerdctl run -d -p 8080:80 nginx:alpine     │
│  nerdctl ps                                 │
│  nerdctl logs web                           │
│  (detached, ports, logs, daily use)         │
└──────────────────────────────────────────────┘

Namespace Separation

┌──────────────────────────────────────────────┐
│  containerd namespaces:                      │
│  ┌────────────────────────────────────────┐  │
│  │ default  ← ctr/nerdctl default         │  │
│  │ k8s.io   ← Kubernetes containers       │  │
│  │ moby     ← Docker containers           │  │
│  └────────────────────────────────────────┘  │
│                                              │
│  A container in "default" is invisible       │
│  when listing "k8s.io"                       │
└──────────────────────────────────────────────┘

Compose with nerdctl

┌──────────────────────────────────────────────┐
│  docker-compose.yaml                         │
│         │                                    │
│         ▼                                    │
│  nerdctl compose up -d                       │
│         │                                    │
│         ▼                                    │
│  containerd creates services, networks,      │
│  volumes from the Compose specification      │
└──────────────────────────────────────────────┘

Summary

ItemValue
ctrLow-level debugging CLI, bundled with containerd
nerdctlDocker-compatible CLI for containerd
Docker CLIFull platform, uses containerd via dockerd
ctr port publishingNot supported
nerdctl port publishingSupported (-p)
nerdctl ComposeSupported (nerdctl compose)
nerdctl buildRequires BuildKit
Kubernetes namespacek8s.io
ctr namespacedefault
Rootlessnerdctl via containerd-rootless-setuptool.sh

Key takeaways:

  • ctr is a debugging tool, not a daily driver. It exposes the raw containerd API—namespaces, content stores, tasks—and lacks port publishing, detached mode, logs, and builds .
  • nerdctl provides the Docker workflow on containerd. It supports run -d -p, ps, logs, exec, build, and compose up, making it the practical replacement for Docker CLI on a containerd-only host .
  • Namespaces are separate. ctr and nerdctl operate in containerd namespaces. Kubernetes containers are in k8s.io, Docker containers are in moby, and the default is default. Listing the wrong namespace makes containers invisible .
  • nerdctl needs dependencies for full functionality. CNI plugins for networking, BuildKit for builds, and RootlessKit for rootless mode. The nerdctl-full tarball bundles them .
  • ctr is always present. It ships with containerd, so it is the tool you can rely on when nothing else is installed. When nerdctl errors, ctr shows the runtime state.
  • crictl is for Kubernetes CRI debugging. It is not Docker-compatible and is focused on inspection, not container creation .

Remember: The shift from Docker to containerd does not mean losing the container workflow. It means choosing the right tool for the job. nerdctl gives you the Docker experience—run, ps, logs, compose, build—on top of the runtime that Kubernetes itself uses. ctr gives you the runtime’s internal perspective: what the content store holds, what the task state is, and why a container will not start. Use nerdctl for your daily work, and reach for ctr when you need to understand what is happening underneath. The Docker CLI remains the most complete development tool, but on a containerd-only system, nerdctl is the closest thing to it, and ctr is the safety net that is always there.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!