In the early days (of containers) docker was a single binary that was the commandline interface (docker run) and the privileged deamon (docker -d). The daemon itself included all the logic,

  • the runtime piece creating the container;
  • a supervisor (of sorts) downloading the container image and snapshotting the image into a filesystem for the container;
  • all the setup and preparation to being able to start a container;
  • even the scheduler SWARM was integrated.

With Kubernetes on the rise the Kubernetes community was not happy (to say the least) that docker was everything at once. Thus, docker was broken up into multiple pieces. Among those was RunC. The only purpose of runC is to create a container. If expects a OCI runtime spec compliant JSON file and a rootfs next to it.


Fetch Image

Same as chroot, we’ll fetch an alpine tarball as our file-system.

if [[ ! -f alpine-minirootfs.tar.gz ]];then 
  export URI=v3.13/releases/x86_64/alpine-minirootfs-3.13.4-x86_64.tar.gz
  wget -qO alpine-minirootfs.tar.gz${URI}
  echo "file already downloaded"


For runc to pick up the root file-system we need to extract the tar ball into a snapshot.

mkdir -p runc/rootfs
tar xfz alpine-minirootfs.tar.gz -C runc/rootfs/
cd runc


runc needs a runtime specification. We’ll use runc itself to create a default one.

runc spec --rootless

Feel free to check out the defaults.


Within the container, we’ll be root.

cat config.json |jq '.process.user'

But the root user and group is mapped to the current user.

cat config.json |jq .linux.uidMappings


runc run alpine-cnt
cat /etc/os-release


Pretty neat already, we are boxed in, running a user-land container without the need to have privileges.

But, we do need to take care of everything like downloading images, snapshot and create the config…