Updated Jul 20, 2026

03 - Docker SDK

📋 Jump to Takeaways

🎁 What if you could do everything the docker CLI does, but with type-safe Go code, no shell parsing, and full programmatic control over containers, images, networks, and volumes?

Imports in code snippets are trimmed for brevity. See the full example linked at the end for complete, compilable source.

Docker Engine API

Docker exposes a REST API over a Unix socket (or TCP). Every docker command maps to an API endpoint. The Go SDK wraps this API with typed structs and methods:

go get github.com/docker/docker/client@latest
go get github.com/docker/docker/api/types@latest
go get github.com/docker/go-connections@latest

The central type is client.Client. It handles connection pooling, API versioning, and authentication to registries.

Connecting to the Docker Daemon

By default, Docker listens on /var/run/docker.sock. The SDK auto-detects this:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/docker/docker/client"
)

func main() {
    // Uses DOCKER_HOST env var, falls back to unix:///var/run/docker.sock
    cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    info, err := cli.Info(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Docker %s, Containers: %d, Images: %d\n",
        info.ServerVersion, info.Containers, info.Images)
    // Output: Docker 24.0.7, Containers: 12, Images: 45
}

For remote Docker daemons over TCP:

cli, err := client.NewClientWithOpts(
    client.WithHost("tcp://10.0.1.50:2376"),
    client.WithTLSClientConfig("/certs/ca.pem", "/certs/cert.pem", "/certs/key.pem"),
    client.WithAPIVersionNegotiation(),
)

WithAPIVersionNegotiation ensures your client works with older daemon versions by auto-downgrading the API version.

Image Operations

Pull an image from a registry, list local images, and tag them:

import (
    "io"

    "github.com/docker/docker/api/types/image"
)

func pullImage(ctx context.Context, cli *client.Client, ref string) error {
    reader, err := cli.ImagePull(ctx, ref, image.PullOptions{})
    if err != nil {
        return fmt.Errorf("pull %s: %w", ref, err)
    }
    defer reader.Close()

    // Must consume the reader or the pull won't complete
    _, err = io.Copy(io.Discard, reader)
    return err
}

func listImages(ctx context.Context, cli *client.Client) error {
    images, err := cli.ImageList(ctx, image.ListOptions{All: false})
    if err != nil {
        return err
    }
    for _, img := range images {
        fmt.Printf("%-60s %dMB\n", img.RepoTags, img.Size/1024/1024)
        // Output: [nginx:latest]                                             142MB
    }
    return nil
}

func tagImage(ctx context.Context, cli *client.Client, source, target string) error {
    return cli.ImageTag(ctx, source, target)
}

The ImagePull response is a stream of JSON progress events. You must read it fully (even to io.Discard) or the pull will stall.

Managing Containers

Create, start, stop, remove, and inspect containers:

import (
    "github.com/docker/docker/api/types/container"
    "github.com/docker/go-connections/nat"
)

func createAndStart(ctx context.Context, cli *client.Client, imageName, containerName string) (string, error) {
    resp, err := cli.ContainerCreate(ctx,
        &container.Config{
            Image: imageName,
            Env:   []string{"APP_ENV=production", "LOG_LEVEL=info"},
            ExposedPorts: nat.PortSet{
                "8080/tcp": struct{}{},
            },
        },
        &container.HostConfig{
            PortBindings: nat.PortMap{
                "8080/tcp": []nat.PortBinding{
                    {HostIP: "0.0.0.0", HostPort: "9090"},
                },
            },
            RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
        },
        nil, // NetworkingConfig
        nil, // Platform
        containerName,
    )
    if err != nil {
        return "", fmt.Errorf("create: %w", err)
    }

    if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
        return "", fmt.Errorf("start: %w", err)
    }

    return resp.ID, nil
}

func stopAndRemove(ctx context.Context, cli *client.Client, containerID string) error {
    timeout := 10 // seconds
    stopOpts := container.StopOptions{Timeout: &timeout}
    if err := cli.ContainerStop(ctx, containerID, stopOpts); err != nil {
        return fmt.Errorf("stop: %w", err)
    }

    return cli.ContainerRemove(ctx, containerID, container.RemoveOptions{
        Force:         true,
        RemoveVolumes: true,
    })
}

func inspectContainer(ctx context.Context, cli *client.Client, containerID string) error {
    info, err := cli.ContainerInspect(ctx, containerID)
    if err != nil {
        return err
    }
    fmt.Printf("Name: %s\nState: %s\nIP: %s\nPID: %d\n",
        info.Name,
        info.State.Status,
        info.NetworkSettings.IPAddress,
        info.State.Pid,
    )
    // Output:
    // Name: /my-api
    // State: running
    // IP: 172.17.0.3
    // PID: 28451
    return nil
}

Streaming Container Logs

Stream logs in real time. This is how docker logs -f works:

func streamLogs(ctx context.Context, cli *client.Client, containerID string) error {
    opts := container.LogsOptions{
        ShowStdout: true,
        ShowStderr: true,
        Follow:     true,
        Timestamps: true,
        Since:      "2024-01-01T00:00:00Z",
    }

    reader, err := cli.ContainerLogs(ctx, containerID, opts)
    if err != nil {
        return err
    }
    defer reader.Close()

    // Docker multiplexes stdout/stderr with an 8-byte header per frame.
    // stdcopy.StdCopy demuxes them.
    _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, reader)
    return err
}

Use stdcopy.StdCopy from github.com/docker/docker/pkg/stdcopy to correctly demultiplex stdout and stderr streams. Raw io.Copy would include the binary frame headers.

Network and Volume Management

Create isolated networks and persistent volumes:

import (
    "github.com/docker/docker/api/types/network"
    "github.com/docker/docker/api/types/volume"
)

func createNetwork(ctx context.Context, cli *client.Client, name string) (string, error) {
    resp, err := cli.NetworkCreate(ctx, name, network.CreateOptions{
        Driver: "bridge",
        Labels: map[string]string{"managed-by": "devctl"},
    })
    if err != nil {
        return "", err
    }
    return resp.ID, nil
}

func createVolume(ctx context.Context, cli *client.Client, name string) error {
    _, err := cli.VolumeCreate(ctx, volume.CreateOptions{
        Name:   name,
        Driver: "local",
        Labels: map[string]string{"managed-by": "devctl"},
    })
    return err
}

func connectToNetwork(ctx context.Context, cli *client.Client, networkID, containerID string) error {
    return cli.NetworkConnect(ctx, networkID, containerID, nil)
}

Attach a volume when creating a container via HostConfig.Binds or Mounts:

&container.HostConfig{
    Mounts: []mount.Mount{
        {
            Type:   mount.TypeVolume,
            Source: "app-data",
            Target: "/data",
        },
    },
}

Container Lifecycle Manager

A self-contained program that demonstrates the full Docker container lifecycle using the Go SDK: pulling an image, creating and starting a container, streaming logs for 5 seconds with proper multiplexed output, then gracefully stopping and removing the container.

Input: A running Docker daemon (connects via DOCKER_HOST or the default socket)

Output: Progress messages for each lifecycle stage, 5 seconds of container logs, and cleanup confirmation

Full source: examples/container-lifecycle

Key Takeaways

  • The Docker Go SDK is the same client the docker CLI uses, giving you full API coverage with type safety
  • Always use client.WithAPIVersionNegotiation() for compatibility with different daemon versions
  • ImagePull returns a stream you must fully consume, even if you discard the output
  • Use stdcopy.StdCopy to demultiplex container log streams. Raw io.Copy corrupts output
  • Pass context.Context everywhere for cancellation and timeouts on long-running operations
  • Clean up containers in defer or with error handling to avoid resource leaks during automation
  • Networks and volumes are first-class API objects you can create, attach, and manage programmatically

🎁 What if you could build and push container images entirely in Go, without a Docker daemon running at all?

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy