Moby is the open-source project behind Docker containers. Here's what Go developers should know about it.
· 3 min read

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:

  1. Clone the Moby repository
  2. Read the contributing guide
  3. Run make to 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.