Containerd ctr Quick Revisit

This quick note is mainly about CLI ctr, please note that ctr tool is made for debugging containerd. It doesn’t support all the features you may be used to from docker such as port publishing, automatic container restart on failure, or browsing container logs.

Recommended containerd course: link.

Containerd Daemon

Assume you have containerd installed:

1
2
3
4
5
6
7
8
9
10
11
sudo systemctl status containerd

● containerd.service - containerd container runtime
Loaded: loaded (/usr/lib/systemd/system/containerd.service; disabled; preset: disabled)
Active: active (running) since Sat 2025-06-14 22:45:52 UTC; 1 week 0 days ago
Docs: https://containerd.io
Main PID: 461 (containerd)
Tasks: 256
Memory: 1.5G
CPU: 4h 43min 43.197s
CGroup: /system.slice/containerd.service

Namespace

You can have both docker and containerd containers running in the same server, docker is also using containerd as the underlying runtime, if you have ctr available you would be able to examine the relationship:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ctr ns ls
NAME LABELS
ns1
ns2

$ ctr -n ns2 c ls
CONTAINER IMAGE RUNTIME
742bd6fc9cd8efdac4dbdf1bd302e4c9ecd1d259224596746939ef5ae0167d47 - io.containerd.runc.v2
ebe97a5c7a289273ce333dae921b7e4f8931cd5edc27749c8b6d445867935a12 - io.containerd.runc.v2

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ebe97a5c7a28 us-docker.pkg.dev/demo/images/example:latest "/mysql-scripts/start" 7 days ago Up 7 days mysqld
742bd6fc9cd8 us-docker.pkg.dev/demo/images/example:latest "/proxy_server -logt…" 7 days ago Up 7 days proxy_server

Container and Task

The separation of containers and tasks in ctr (and in containerd’s architecture) might seem a bit different from Docker’s more unified view of a running container.

A container in containerd is primarily an isolated metadata and configuration entity. It doesn’t have a running process (task) associated with it yet. So you can have a container in containerd without anyone live task(the init process) running, it is just empty.

Please note that in containerd, one container has only one task (init process), one task can have more than one processes!

Commands

Image Pull

Let’s see an example to demonstrate the container vs task in containerd, I have the ctr alias setup like below for target namespace:

1
alias ctr='sudo ctr -n ns1'

Please note that even the image is also associated with namespace! Also You have to pull it first, ctr won’t do it auto for you when you create a container:

1
$ ctr image pull docker.io/library/alpine:latest

List the local images:

1
2
3
4
# -q: only show image path
ctr i ls -q

docker.io/library/alpine:latest

Inspect Image Internals

Instead of running a container with task and exec into it, one way to check the image internal (e.g the built-in files) is to mount a local folder to image and go to check the local folder:

1
2
3
4
5
6
7
8
9
$ mkdir /tmp/agent_rootfs
$ ctr i mount docker.io/library/alpine:latest /tmp/agent_rootfs

# now examine the alpine image internals
$ ls /tmp/agent_rootfs
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

# unmount
$ ctr i unmount /tmp/agent_rootfs

Create Container

Then, create a container named as test and give it a default process /bin/sh, if you don’t specify a command, it might use the default command from the image’s configuration (which might just exit immediately if it’s not designed to run indefinitely).

1
ctr c create docker.io/library/alpine:latest test /bin/sh

Check the container is created:

1
2
3
$ ctr c ls

test docker.io/library/alpine:latest

You can view the arg bin/sh we specified in args field:

1
ctr c info test | grep -A3 args

Sometimes you want to check the container start timestamp:

1
ctr c info test | jq -r '.UpdatedAt'

Please note that no task is running from test container so far:

1
2
# you should see empty result
ctr t ls | grep test

Start Container

Now start the container in detached mode, this command starts the initial task associated with the container test. By default, the init task ID will be the same as the container ID, the /bin/sh is the init process of the task

1
2
3
4
$ ctr t start -d test

$ ctr t ls | grep test
test 125090 RUNNING

Create Children Process

Let’s create 3 processes for task test:

1
2
3
ctr t exec -d  --exec-id pro1 test /bin/sh
ctr t exec -d --exec-id pro2 test /bin/sh
ctr t exec -d --exec-id pro3 test /bin/sh

Now check the processes in container test, please note we still have only one task ctr t ls | grep test:

1
2
3
4
5
6
7
$ ctr t ps test

PID INFO
125090 -
125191 exec_id:"pro1"
125221 exec_id:"pro2"
125250 exec_id:"pro3"

The PID here like 125191, 125221, 125250 is the host PID of the process, so you kill them from host, also ps -aux | grep can be used to find the process in host VM, to kill the process pro1:

1
sudo kill -9 125191

Exec into Container

To exec into the container, you need to specify a process name:

1
2
# foo ps name
ctr t exec -t --exec-id foo test sh

Or using a random ps name:

1
ctr t exec -t --exec-id ${RANDOM} test sh

Remove

Finally, kill the task test:

1
ctr t kill -s 9 test

Clean up the task test:

1
ctr t rm -f test

Clean up the container test:

1
ctr c rm test

Containerd Container Lifecycle Management

As we mentioned containerd or ctr does not have a restart on failure config like docker, one way to restart on failure is to set up systemd service, for example:

1
2
3
4
5
6
7
8
9
$ systemctl status example.service

● example.service - Example: app
Loaded: loaded (/etc/systemd/system/example.service; disabled; preset: disabled)
Active: active (running) since Sat 2025-06-14 22:46:54 UTC; 1 week 1 day ago
Main PID: 2965 (bash)
Tasks: 2 (limit: 17980)
Memory: 2.0M
CPU: 45.258s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat /etc/systemd/system/example.service

[Unit]
Description=Example: app
Wants=example-setup-vmparams-noncritical.service
After=example-setup-vmparams-noncritical.service
[Service]
User=example
Restart=always
RestartSec=5
StartLimitBurst=10000
ExecStart=/bin/bash /var/lib/example/noncritical/bin/manage.sh start
ExecStop=-/bin/bash /var/lib/example/noncritical/bin/manage.sh stop
[Install]
WantedBy=example-non-critical.target
0%