Preface
The Open Container Initiative (OCI) defines open industry standards for containers. OCI currently includes three specifications:
- Runtime Specification (runtime-spec)
- Image Specification (image-spec)
- Distribution Specification (distribution-spec)
I will introduce the three OCI specifications in a series of articles, along with related software and tools, such as the widely used container runtime runc. This article is the second in the series, where we will actually use runc to manage a container. For some important concepts about the runtime-spec, see the first article: OCI - runtime spec introduction.
Overview
In this article, we will design a container that contains a "hello-world" main
program, as well as a createContainer hook and a postStop hook. The main
program source code is as follows. The hooks are implemented using the existing
Linux touch command. They respectively create the files /tmp/createContainerHook
and /tmp/postStopHook.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Hello World")
time.Sleep(10 * time.Second)
}
The main operations covered in this article include:
- Preparing the filesystem bundle
- Using runc to create a container
- Using runc to start a container
- Using runc to delete a container
Preparing the Filesystem Bundle
In the first article, we mentioned that an OCI container runtime (such as runc)
creates containers based on a directory called a filesystem bundle. A
filesystem bundle is essentially a directory containing two things: a configuration
file called config.json and a container root filesystem. A bundle can be converted
from a container image, or it can be manually created from scratch. In real use
cases, bundles are usually generated from images by software such as containerd.
Here, however, we will build one manually to show that it is not so mysterious.
- First, create the filesystem bundle directory:
$ mkdir -p /tmp/filesystem-bundle-demo
- Next, prepare the container root filesystem and copy our hello-world program into the proper location:
$ cd /tmp/filesystem-bundle-demo
$ mkdir -p rootfs/usr/local/bin/
# Copy the binary into "/usr/local/bin". The structure is:
$ tree .
.
└── rootfs
└── usr
└── local
└── bin
├── hello-world
- Then generate a default
config.jsonwith runc and modify it accordingly. The modifications mainly include changing the container startup command to ourhello-worldprogram and adding hook configurations. Note that hook commands run on the host, not inside the bundle. Hooks often perform tasks that modify the container’s filesystem bundle, so they are external. The modified file looks like this (note some content are removed for brevity):
$ runc spec --rootless
# Edit config.json to change startup command and add hooks, final content:
$ cat config.json
{
"ociVersion": "1.0.2-dev",
"hooks": {
"createContainer": [
{
"path": "/usr/bin/touch",
"args": ["touch", "/tmp/createContainerHook"]
}
],
"postStop": [
{
"path": "/usr/bin/touch",
"args": ["touch", "/tmp/postStopHook"]
}
]
},
"process": {
"terminal": false,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"hello-world"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": {
"bounding": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"effective": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"permitted": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"ambient": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
},
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "runc",
"linux": {
"uidMappings": [
{
"containerID": 0,
"hostID": 1001,
"size": 1
}
],
"gidMappings": [
{
"containerID": 0,
"hostID": 1001,
"size": 1
}
],
"maskedPaths": [
"/proc/acpi",
"/proc/asound",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi"
]
}
}
At this point, we have prepared the so-called filesystem bundle. You can see that it contains very little — just our statically compiled hello-world binary in the root filesystem.
Creating a Container with runc
With the filesystem bundle ready, we can create a container. The following commands
create the container and check its state. We see the container in the created
state, and the file /tmp/createContainerHook exists while /tmp/postStopHook
does not, meaning the createContainer hook was executed at container creation,
but the postStop hook was not. This is as expected.
Also, from the tree command output, we see that extra directories like dev,
proc, and sys appear inside the container root filesystem. These are mounts
that runc sets up by default, as specified in the default config.json.
$ runc create --bundle ./ demo
$ runc list
ID PID STATUS BUNDLE CREATED OWNER
demo 1928558 created /tmp/filesystem-bundle-demo 2024-12-12T06:35:03.212069773Z *
$ runc state demo
{
"ociVersion": "1.0.2-dev",
"id": "demo",
"pid": 1928558,
"status": "created",
"bundle": "/tmp/filesystem-bundle-demo",
"rootfs": "/tmp/filesystem-bundle-demo/rootfs",
"created": "2024-12-12T06:35:03.212069773Z",
"owner": ""
$ ls /tmp/{createContainerHook,postStopHook}
ls: cannot access '/tmp/postStopHook': No such file or directory
/tmp/createContainerHook
$ tree .
.
├── config.json
└── rootfs
├── dev
├── proc
├── sys
└── usr
└── local
└── bin
└── hello-world
Starting a Container with runc
After creation, we can start the container, which actually runs the user program.
We see our hello-world program running and printing Hello World. If you watch
closely, the container status first becomes running, then stopped, corresponding
to the execution and exit of the program.
At this stage, the postStop hook has still not been executed.
$ runc start demo
Hello World
$ runc list
ID PID STATUS BUNDLE CREATED OWNER
demo 0 stopped /tmp/filesystem-bundle-demo 2024-12-12T06:35:03.212069773Z *
$ runc state demo
{
"ociVersion": "1.0.2-dev",
"id": "demo",
"pid": 0,
"status": "stopped",
"bundle": "/tmp/filesystem-bundle-demo",
"rootfs": "/tmp/filesystem-bundle-demo/rootfs",
"created": "2024-12-12T06:35:03.212069773Z",
"owner": ""
}
$ ls /tmp/{createContainerHook,postStopHook}
ls: cannot access '/tmp/postStopHook': No such file or directory
/tmp/createContainerHook
Deleting a Container with runc
Finally, when we no longer need the container, we can delete it. At this point,
the postStop hook gets executed:
$ runc delete demo
$ ls /tmp/{createContainerHook,postStopHook}
/tmp/createContainerHook /tmp/postStopHook
Summary
This article demonstrated, through a simple example using runc, how containers are managed at the OCI runtime layer. Of course, this was just the basic flow, meant to give you an initial understanding. Advanced topics will be covered in later articles. If you’re unclear on concepts such as filesystem bundles or hooks, refer to OCI - runtime spec introduction.
If you’re using other OCI-compliant runtimes such as crun or youki, you can try similar steps.