Build Your Own Docker: Executing a process [1/2]
This is the first article in a series where we’ll build a toy Docker clone in Go.
Previous article: Introduction
Next article: Isolating a process
Sections in this article:
Skeleton
The docker clone we’ll build will be able to pull an image from docker.io and run a command inside it.
It’ll be invoked like this:
$ toy-docker run alpine echo "hello world"
Let’s ignore details about the Docker registry, Docker image etc. for now and
focus on just one thing here: How do we execute the echo
program and ensure it
is sufficiently isolated from the outside world?
package main
import (
"fmt"
"os"
)
func main() {
// Let's ignore the 'run alpine' part for now, and focus on executing the
// command provided.
if os.Args[1] != "run" || os.Args[2] != "alpine" {
fmt.Println("Expected 'run alpine <command>' as the command!")
}
// TODO: Execute the command given in os.Args[3]!
}
Basic ‘exec’
On Linux-based systems, the exec
syscall (a family of
calls, rather) can be used to replace the
calling process with a new one.
Let’s use exec
to execute the command given:
package main import ( "fmt" "os"+ "syscall" ) func main() { // Let's ignore the 'run alpine' part for now, and focus on executing the // command provided. if os.Args[1] != "run" || os.Args[2] != "alpine" { fmt.Println("Expected 'run alpine <command>' as the command!") } - // TODO: Execute the command given in os.Args[3]!+ if err := syscall.Exec("/usr/bin/"+os.Args[3], os.Args[3:], os.Environ()); err != nil {+ fmt.Printf("Error: %v", err)+ os.Exit(1)+ } }
Let’s test this with toy-docker run alpine echo hey
:
➜ toy-docker run alpine echo hello
hello
Seems to work just fine! We can run any arbitrary command, as long as it is available on the host system.
➜ rohitpaulk.com git:(master) ✗ toy-docker run alpine curl httpbin.org/get
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.67.0"
},
"origin": "122.177.35.168, 122.177.35.168",
"url": "https://httpbin.org/get"
}
Note: To make the main.go
file runnable as toy-docker
, I used a quick and
dirty hack. I aliased toy-docker
to docker run main.go
.
alias toy-docker="go run main.go"
The proper way to do this is probarbly to create a package named toy-docker
and run go install
in it.
The need for isolation
Our version of toy-docker
can execute any command given to it. That’s a
bit too powerful!
For example, toy-docker
can find the directory it was executed from, and list
files on the host.
➜ toy-docker run alpine pwd
/home/rohitpaulk/experiments/productions/rohitpaulk.com
➜ toy-docker run alpine ls
404.html articles articles.html _config.yml css _data elsewhere.html _files Gemfile Gemfile.lock images index.md js _layouts Makefile now.md _plugins reading.html _scripts _site
That’s not good! If I did the same with docker
, it’d run in the root folder of
what seems like an entirely new machine.
➜ docker run alpine pwd
/
➜ docker run alpine ls
bin
dev
etc
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
toy-docker
can also list the processes on the host machine.
➜ toy-docker run alpine ps
PID TTY TIME CMD
9516 pts/1 00:00:00 zsh
15966 pts/1 00:00:00 go
16024 pts/1 00:00:00 ps
It can terminate other processes too!
➜ toy-docker run alpine pkill -9 zsh
<poof!> my shell vanishes
Running the same with docker
, we can see that it doesn’t have the same
privileges.
➜ docker run alpine ps
PID USER TIME COMMAND
1 root 0:00 ps
The process seems to be running in an isolated environment. A ‘container’, if you will.
In the next article, we’ll look at the isolation mechanisms that Docker uses.