Containers are moving the world. Each passing day more and more organizations are embracing containers as first-class citizens for distribution and deployment of software components. Containers represent the core of the cloud native paradigm. We are witnessing the birth of new container runtimes such as CRI-O - a lightweight alternative to Docker that allows spinning up containers on top of Kubernetes. A while back, I blogged about container internals and the Linux pillars that power some of the leading container engines. I have to admit the code snippets were rather unfunctional and ugly. Interfacing with the underlying Linux subsystem through libc crate with unsafe scattered throughout the code looked terrifying. That’s why I decided to wrap up a small project called rabbitc - a minimal container runtime meant for learning purposes (keep in mind that my knowledge on Rust is modest and I’m still in process of educating myself). Rust toolchain is required to build it. Once you have the binary, grab a minimal rootfs (such as Alpine’s rootfs) and spawn a new containerized process.

$ sudo rabbitc --rootfs=<rootfs>
/# ps
PID   USER     TIME   COMMAND
1     root     0:00   sh
5     root     0:00   ps

By default, you’ll be presented with a shell process, but you can run a different process with --cmd option. For example, let’s build a Go-based HTTP server and run it inside container. Save the following code snippet to main.go.

package main

import (
    "net/http"
    "fmt"
    "net"
)

func main() {
   http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
      fmt.Fprintf(w, "Hello from rabbitc container")
   })
   var addr string
   addrs, err := net.InterfaceAddrs()
   if err != nil {
       addr = "127.0.0.1"
   }

   for _, address := range addrs {
      if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
            if ipnet.IP.To4() != nil {
                addr = ipnet.IP.String()
            }
      }
   }
   endpoint := fmt.Sprintf("%s:80", addr)
   fmt.Println("running HTTP server on", endpoint)
   err = http.ListenAndServe(endpoint, nil)
   if err != nil {
      panic(err)
   }
}

Instruct the Go compiler to produce statically linked binary by disabling CGO. We also copy the resulting binary to our rootfs path.

$ CGO_ENABLED=0 go build -o server main.go
$ cp server /<rootfs>/bin

Since rabbitc doesn’t support port forwarding (but it’s easily doable with some iptables magic), we have to provide container IP address to send the request to HTTP server.

$ sudo rabbitc --rootfs=<rootfs> --cmd=/bin/server
Running HTTP server on 172.19.0.2:80
$ curl 172.19.0.2:80
Hello from rabbitc container

We’re running a full isolated process with its own network stack, file system and process tree in 400~ lines of Rust code. That’s awesome! In the next blog post I’ll focus on explaining the remaining kernel namespaces, how resources are controlled by cgroup subsystem and other container-specific nuances. Stay tuned.