Did you know Docker is actually built on Moby?
You probably typed docker at least once today. What most people don’t realise is that Docker the product is assembled from an open source Go project called Moby, and the interesting engineering lives there.
I’m a big believer that reading other people’s code is one of the best ways to improve at Go, and Moby rewards that time more than most repositories. Here’s why it’s worth a look.
What is Moby?
Moby is a collaborative project providing the components you need to build container-based systems. Docker Inc. spun it out in 2017 to separate the open source container technology from the commercial Docker product.
The comparison that made it click for me: Docker is to Moby what Chrome is to Chromium. Moby provides the building blocks. Docker assembles them into something you’d install on your laptop.
The project is written almost entirely in Go, which makes it a great place to see how Go copes with serious systems programming.
Why Go for containers?
Containers are mostly Linux primitives wearing a trench coat: namespaces, cgroups, union filesystems. Go is unusually good at talking to those primitives directly. Here’s the kind of thing Moby does with the syscall package:
package main
import (
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("/bin/sh")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
if err := cmd.Run(); err != nil {
panic(err)
}
}
That’s a shell running with isolated UTS, PID, and mount namespaces. The skeleton of container isolation in a few dozen lines, with no C bindings and no ceremony.
The client-server architecture
Moby uses a client-server model: the Docker CLI you type into is a thin client talking to the Docker daemon over a REST API. Which means anything the CLI can do, your Go program can do too:
package main
import (
"context"
"fmt"
"github.com/docker/docker/client"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}
defer cli.Close()
containers, err := cli.ContainerList(ctx, container.ListOptions{})
if err != nil {
panic(err)
}
for _, c := range containers {
fmt.Printf("Container: %s - %s\n", c.ID[:12], c.Image)
}
}
Notice how context is used to manage the request lifecycle. This pattern is everywhere in Moby’s codebase, and for good reason: a daemon managing long-running operations needs cancellation and timeouts to be first class, not bolted on.
Building images programmatically
You can go further and build images straight from Go, which is handy for CI/CD pipelines or custom tooling:
package main
import (
"archive/tar"
"bytes"
"context"
"io"
"os"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}
defer cli.Close()
// Create a tar archive with Dockerfile
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
dockerfile := `FROM alpine:latest
RUN echo "Hello from Go-built image"
`
header := &tar.Header{
Name: "Dockerfile",
Size: int64(len(dockerfile)),
}
tw.WriteHeader(header)
tw.Write([]byte(dockerfile))
tw.Close()
// Build the image
resp, err := cli.ImageBuild(ctx, buf, types.ImageBuildOptions{
Tags: []string{"my-go-image:latest"},
Dockerfile: "Dockerfile",
})
if err != nil {
panic(err)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
Tarring up a Dockerfile in memory feels odd the first time you do it, but that is genuinely how the API works: the build context travels to the daemon as a tar stream.
Lessons from Moby’s codebase
A few things stood out to me reading the source. Components talk to each other through interfaces, which is how the same daemon supports different platforms without drowning in build tags. Shutdown is taken seriously: when you stop Docker it cleans up running containers rather than orphaning them, a discipline that pairs well with careful error handling. And the plugin system for storage, networking, and logging shows how far Go’s interfaces stretch without needing a formal plugin framework.
Getting started with Moby development
Want to see for yourself? Start here:
- Clone the Moby repository
- Read the contributing guide
- Run
maketo build the project
The codebase is large but well organised. I’d start with the client package to understand the API surface, then move on to daemon to see how containers actually come to life.
Wrapping up
Moby quietly shaped how a generation of Go programmers write daemons: context propagation, interface-driven design, graceful shutdown. Set aside half an hour, clone it, and read. The next time Docker does something you don’t understand, you’ll know where the answer actually lives.