runc, crun & Container Standards Wrap Up

Illustration of two hot air balloons floating over a gray landscape. The larger balloon has "CRUN" written on it with a person in the basket looking back. The smaller balloon has "RUNC" on it, carrying a basket with rocks. Background features a cloudy sky.
Last updated: | Published:
Illustration of two hot air balloons floating over a gray landscape. The larger balloon has "CRUN" written on it with a person in the basket looking back. The smaller balloon has "RUNC" on it, carrying a basket with rocks. Background features a cloudy sky.

There are many tools to work with containers.

We already know about container images and container bundles, and we even know that we don't need container image to run a container. More over, we even have a container bundle that we do need to run a container. The last step in our OCI journey is to finally run it!

For this, we are going to use a container runtime.

As a reminder, we need a container runtime to bind together different bits and pieces that make up a container: cgroups, user namespaces, process namespaces, various security mechanism like SELinux and Linux Capabilities and so on, and then finally start a container process.

The most popular container runtime right now is runc. If you look under Docker, Podman or many other container managers, you will notice that all of them in the end spawn containers with the help of runc - we will examine this ourselves in a later chapter. But first, let's get runc running.

To install runc, you can either install the runc package or containerd package, which includes runc inside it.

Running containers with runc

In the previous lesson, we've prepared our container bundle - we have both the runtime-spec config file and a root filesystem. This is enough to start a new container with the help of the runc. Let's give it a try.

To run the container, we need to use the runc run command, followed by the container name. runc run is much simpler than Docker run - it doesn't accept dozens of arguments for volumes, environment variables and so on:

runc run test

We will get an error:

AH00557: httpd: apr_sockaddr_info_get() failed for umoci-default
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1. Set the 'ServerName' directive globally to suppress this message
(13)Permission denied: AH00072: make_sock: could not bind to address [::]:80
(13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down
AH00015: Unable to open log

That’s because our config file tries to bind a reserved port, which is every port under 1024 - only root can do that by default. We could either run the container with sudo, adjust our runtime specification or modify the apache configuration to listen on a different port. Lets do the latter.

vi rootfs/usr/local/apache2/conf/httpd.conf

and change the port from 80 to 8091.

Now start the container again - there are lots of different warnings in the log, but the httpd is running just fine - we can confirm that by running curl localhost:8091 in a separate terminal tab, or by opening this address in the browser. Congratulations, you’ve created your first completely Dockerless container! And while that’s for sure is not the easiest way to start a container, at least we know all the lower level details about how it works.

Now let’s examine few more things about runc.

The first thing that you might have noticed, is that runc container is attached to the terminal. It’s good in certain scenarios, but certainly not the way we would run a container in production. By default runc runs in foreground mode, which also means it’s a direct child of our shell session. Confirm it by running ps --forest -x:

  7168 pts/0 Ss 0:06 | \_ zsh
 306726 pts/0 Sl+ 0:00 | | \_ runc run test
 306737 pts/0 Ss+ 0:00 | | \_ httpd -DFOREGROUND
 306746 pts/0 Sl+ 0:00 | | \_ httpd -DFOREGROUND
 306747 pts/0 Sl+ 0:00 | | \_ httpd -DFOREGROUND
 306749 pts/0 Sl+ 0:00 | | \_ httpd -DFOREGROUND

Let’s kill our container and this time run it with --detach option:

ERRO\[0000\] cannot allocate tty if runc will detach without setting console socket

We get this error, because our runtime config has an option terminal set to true - runc can’t attach container to the terminal, if the container is detached . Set the terminal option to false and try again!

If we check the process table now, we will see that there is no direct parent of the container. It also means that there is no runc daemon to track this container. Instead, runc stores a simple json file with the container state in /run/user/YOUR\_UID/runc/CONTAINER\_NAME/state.json - this file has the container process id and different information about the container.

This state file also allows us to run different commands on the existing container. For example, we can run runc exec test whoami - this will give as root, meaning that inside container the user is indeed the root one. We can also enter the container with runc exec --tty test sh if we want to.

runc applications

We’ve run our first container with the help of runc. We had to do some strange things - modify the files directly to adjust the port, inspect process tree and so on. None of things is close to the convenience of using Docker to manage containers - so why bother with runc?

We have to look at this picture to understand why knowing runc is important:

Diagram showing user interactions leading to container runtimes: Docker CLI via Docker Daemon to Containerd to runc, Podman to runc, Cri-o to runc, and Containerd to runc, all with arrows indicating flow.

Runc is the default container runtime for practically any container manager out there. Every container you run with Docker, Podman, Cri-o or Containerd is, down the stack, launched with runc. It also means that every other tool is just an extra convenience layer on top of the container runtime. This is why it’s good to at least keep in mind the existence of runc and which role it plays in your containerized environment.

You could, of course, wrap runc with systemd units and some shell scripts and, this way, avoid installing the proper container manager. But that would not make too much sense. The curiosity of runc, though, is that it really is a CLI tool. There is no SDK in other programming languages that would allow you to integrate what runc does into your containers. Instead, all the aforementioned tools, essentially, wrap the CLI calls to runc when they need to start, stop, or get into the container.

It might seems like, in this case, that everything depends on this single little binary called runc. But in reality, it’s not. runc is just the standard and most popular implementation of runtime-spec, but you can use any other implementation - you can even swap the container runtime that is used by those higher level tools.

Alternative OCI runtime - crun

I’ve mentioned that it does not matter which container runtime you use, as long as it complies with the runtime-spec. In this chapter, we will look at another runtime called crun. And no, it’s not a typo.

crun is a fast and lightweight fully featured OCI runtime and C library for running containers. It’s available in the default repositories.

Using crun is exactly the same as runc. Let’s run our container bundle with httpd:

crun run test

It will start the new container, and we can access the webserver on port 8091!

Just like runc, crun stores a state file, only at a different location - /run/user/1000/crun/test/config.json. Other than that, it’s basically a completely container runtime, implemented in a different programming language, but totally capable of doing exactly the same things as runc - as long as the container bundle is OCI compliant.

Container Standards Wrap Up

We’ve learned quite a lot of low level things. Let’s re-cap them.

We’ve started by looking at the Open Container Initiative - an organization and a set of open standards around containers.

OCI defines multiple specifications.

We’ve looked at image-spec - the definition of how to build and manage container images. We’ve used Skopeo, a small utility focused on working with container images, to inspect an existing image. An image consists of various configs and manifests, as well as tar-packaged layers.

We’ve then examined runtime-spec and container bundles. Container images are always unpacked into container bundles, which consist of a root filesystem and a runtime-spec config file.

To get the bundle, we’ve used umoci - a tool that allowed us to take a container image and unpack it into a container bundle.

Finally, we’ve run this bundle with a container runtime called runc and looked a bit at how containers are running at the lowest level. We even tried an alternative runtime called crun - and proved that no matter which runtime we use, our OCI-compliant bits and pieces stay the same.

This was a very low level look at the containers. Most importantly, we were never bound to using Docker. All of the things we saw, except the Docker Image Format, are Dockerless - meaning, they are not about single tool, they are all about standards, conventions and specifications.

But working at such a low level is inconvenient. We would never build images by assembling them by hand, and never unpack bundles by scripting umoci. After all, the main benefit of Docker is that it makes containers easy to use.

Till this point, we learned how containers work and we learned how to make containers harder to use. In the second half of the course, we will learn how to use containers with the same convenience as Docker, but without any Docker in sight.