Skip to main content

Command Palette

Search for a command to run...

Understanding Containers by Building One: Go Container Runtime Tutorial

Published
4 min read

What if I ask you that do you use docker? The answer of most people of will be yes ! if some people answer is no then they might have use other container runtime tools like podman, CRI-O . But the generic answer of people will be yes . In today’s era of micro-services we are forced to use container runtime tools . Here, we are not going to discuss the befit of using CRI, cause that’s not our motive for this blog ! Rather than we will see how container runtime tools work under the hood !
In this blog we will show a demo how to build a container runtime and we encourage you to get your hands dirty into it !

What You'll Learn

  1. Linux namespaces and process isolation

  2. Container runtime fundamentals

  3. Go systems programming

  4. How Docker-like tools work internally

Prerequisites

You should know the basic of Linux environment and basic of go programming language and what is root access ! the basic understanding is enough to get into . If you wonder that how you can learn about this don’t worry I’m giving you the list of resource that are need to for this
1. https://www.redhat.com/en/blog/7-linux-namespaces
2. https://www.youtube.com/watch?v=sK5i-N34im8
3. https://devoriales.com/post/318/understanding-kubernetes-container-runtime-cri-containerd-and-runc-explained
more and less those Blogs and YouTube videos are enough for our tutorials !

What We're Building

A minimal container runtime that can 1) Isolate processes using namespaces 2) Set custom hostnames 3) Execute commands in isolated environments 4) Provide basic filesystem isolation

Project Setup

So, we are at the beginning of starting , I’m sure that you will learn something new here , Let’s don’t waste much time grab your coffee and open your terminal.

mkdir my-container-runtime
cd my-container-runtime
go mod init my-container-runtime

Step 1: Create the Entry Point (main.go)

Create a file named main.go by
touch main.go

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        log.Fatal("Usage: ./my-container run <command>")
    }

    switch os.Args[1] {
    case "run":
        if len(os.Args) < 3 {
            log.Fatal("Usage: ./my-container run <command>")
        }
        runContainer(os.Args[2:])
    case "child":
        runChild()
    default:
        log.Fatal("Unknown command:", os.Args[1])
    }

}

func runContainer(args []string) {
    fmt.Printf("Running container with command: %v\n", args)

    container := &Container{
        Command: args,
    }

    if err := container.Run(); err != nil {
        log.Fatal(err)
    }
}

func runChild() {
    fmt.Println("Running inside container!")

    if err := setupNamespaces(); err != nil {
        log.Fatal("Failed to setup namespaces:", err)
    }

    if err := setupFilesystem(); err != nil {
        log.Fatal("Failed to setup filesystem:", err)
    }

    if err := runCommand(); err != nil {
        log.Fatal("Failed to run command:", err)
    }
}

This Go program is a toy container runtime (like a super-mini Docker).
It can:

  1. Take a command from the user (./my-container run <command>).

  2. Create a child process in isolated namespaces (container-like).

  3. Set up file system isolation.

  4. Execute the command inside that container.

Step 2: Implement Container Logic (container.go)

Now let's create the core container functionality:
touch container.go

package main

import (
    "os"
    "os/exec"
    "syscall"
)

type Container struct {
    Command []string
}

func (c *Container) Run() error {

    cmd := exec.Command("/proc/self/exe", append([]string{"child"}, c.Command...)...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWNS |
            syscall.CLONE_NEWPID |
            syscall.CLONE_NEWNET |
            syscall.CLONE_NEWUTS,
        Unshareflags: syscall.CLONE_NEWNS,
    }

    return cmd.Run()
}

func runCommand() error {
    args := os.Args[2:]
    if len(args) == 0 {
        args = []string{"/bin/sh"}
    }

    // Execute the command
    return syscall.Exec(args[0], args, os.Environ())

}

Key Concepts Explained:

  1. CLONE_NEWPID: Process isolation (container sees different process IDs)

  2. CLONE_NEWUTS: Hostname isolation

  3. CLONE_NEWNS: Mount point isolation

  4. CLONE_NEWNET: Network isolation

5. syscall.Exec: Replace current process with target command

Step 3: Add Namespace Isolation (namespace.go)

touch namespace.go

Let's implement the namespace setup:

package main

import (
    "os"
    "syscall"
)

func setupNamespaces() error {
    if err := syscall.Sethostname([]byte("container")); err != nil {
        return err
    }
    if err := os.Chdir("/"); err != nil {
        return err
    }

    return nil

}

What this does:

  1. Sets container hostname to "my-container"

  2. Changes to root directory for clean slate

  3. Provides foundation for further isolation

Step 4: Basic Filesystem Setup (filesystem.go)

touch filesystem.go

Add basic filesystem isolation:

package main

import (
    "os"
    "syscall"
)

func setupFilesystem() error {
    dirs := []string{"/proc", "/sys", "/dev", "/tmp"}

    for _, dir := range dirs {
        if err := os.MkdirAll(dir, 0755); err != nil {
            continue
        }
    }

    if err := syscall.Mount("proc", "/proc", "proc", 0, ""); err != nil {
    }

    // Mount sysfs
    if err := syscall.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
    }

    return nil
}

Understanding the filesystem:

  1. Creates essential directories

  2. Mounts proc filesystem for process visibility

  3. Mount failures are expected (and okay) in basic implementation

  4. Provides foundation for container filesystem isolation

Cool you have just build your own container ! now if you wonder that we just write code but did not test it properly then your thinking is valid . Well please refer to my github readme file for this
https://github.com/Rupam-It/container-run-interface/blob/main/README.md

Thanks everyone who ever reading this line please follow me on linkedin . And if you wonder that you can contribute to this project more feel free to open an PR. I will love to hear you back with new implementation and idea! Thanks once again meet with you again in next blog!

More from this blog

kube Know

11 posts