API

Before we talk about the theory of and implementation behind containers, let’s first look at the API for my toy container.

At the core, the mini-container program takes two arguments: an executable program and a directory that points to a root filesystem. It creates a process, sets up the container environment for the process, and executes the executable program in this container.

Here are the arguments and options to execute my toy container.

mini-container [OPTIONS] <PATH_TO_EXECUTABLE> <ROOT_FILESYSTEM_PATH>

Arguments:

  • <COMMAND> Command to execute
  • <ROOT_FILESYSTEM_PATH> Absolute path to the new root filesystem

Options:

-p, --pid <PID> Set the pid for child process

-m, --memory <MEMORY> Memory limit (megabytes)

--nproc <NPROC> Max pids allowed

-u, --user <USER> Set the User ID for child process

--cap-add <CAP_ADD> Add Linux capabilities to the container environment

--cap-drop <CAP_DROP> Drop Linux capabilities to the container environment. Specify “ALL” to drop all

-h, --help

Examples

Running an interactive bash shell

To run an interactive bash shell in the container environment, you first need to set up a directory that will serve as the root filesystem for the container. This is equivalent to an image in Docker, which contains a minimal OS. For all my demos, I will be using Alpine’s Mini Root Filesystem image.

First, we download the image and extract it into the alpine directory.

cd /home/brianshih
# download the alpine image
wget <https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/aarch64/alpine-minirootfs-3.19.0-aarch64.tar.gz>
# create the new_root directory
mkdir alpine
# extract the alpine image into the new_root directory
tar -xvf alpine-minirootfs-3.19.0-aarch64.tar.gz -C alpine

Next, we can launch the container and execute /bin/ash. Note that the alpine directory will become the new root filesystem.

sudo target/debug/mini-container /bin/ash /home/brianshih/alpine

Here is the rough equivalent command in docker:

docker exec -it alpine bash

Limiting resources in the container

You can run a container with limited memory and limited process capacity via the --nproc and --memory options.

sudo target/debug/mini-container /bin/ash /home/brianshih/alpine 
	--nproc 5 --memory 1048

Here is the rough equivalent command in docker - though unlike my implementation, nproc in Docker sets the maximum number of processes available to a user, not to a container.

docker run --memory="1048m" --ulimit nproc=5 IMAGE

Dropping and Adding Linux Capabilities

Here is how you can drop all the Linux capabilities and add the NET_BIND_SERVICE capability. Note that for my toy implementation, I only support 3 capabilities (so far). It’s extremely trivial to add them but my goal isn’t to build a production-level container so I stopped whenever I felt like I understood how they work.

sudo target/debug/mini-container /bin/ash /home/brianshih/alpine 
	--cap-drop ALL 
	--cap-add NET_BIND_SERVICE

Here is the rough equivalent command in docker:

docker run --cap-drop all --cap-add NET_BIND_SERVICE alpine

Setting the User ID

Here is how you can set the user ID for the process.

sudo target/debug/mini-container /bin/ash /home/brianshih/alpine --user 0

Here is the rough equivalent command in docker:

docker run --rm --user $UID:$GID alpine ash