Understanding Containers by Building One: Go Container Runtime Tutorial
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
Linux namespaces and process isolation
Container runtime fundamentals
Go systems programming
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 bytouch 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:
Take a command from the user (
./my-container run <command>).Create a child process in isolated namespaces (container-like).
Set up file system isolation.
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:
CLONE_NEWPID: Process isolation (container sees different process IDs)CLONE_NEWUTS: Hostname isolationCLONE_NEWNS: Mount point isolationCLONE_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:
Sets container hostname to "my-container"
Changes to root directory for clean slate
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:
Creates essential directories
Mounts proc filesystem for process visibility
Mount failures are expected (and okay) in basic implementation
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!


