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@latestThe 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
dockerCLI uses, giving you full API coverage with type safety - Always use
client.WithAPIVersionNegotiation()for compatibility with different daemon versions ImagePullreturns a stream you must fully consume, even if you discard the output- Use
stdcopy.StdCopyto demultiplex container log streams. Rawio.Copycorrupts output - Pass
context.Contexteverywhere for cancellation and timeouts on long-running operations - Clean up containers in
deferor 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?