~ / Blog / Interacting With the Docker Engine in Go
Lately, I've been hacking on a little side project called Vauban (when not spending time job hunting).
It's also a good excuse to play with Docker's Go SDK. Since Docker itself is written in Go, we are just a few imports from getting a nice SDK. Cool, right?
This guide walks through a small Go program that interacts with the Docker Engine. We'll compare it with equivalent CLI commands and see how you can build a basic Docker playground in Go.
You can find the full source here.
And here is the end result we'll get:
First, create a new folder and initialize a Go module:
mkdir docker-playground && cd docker-playground
go mod init github.com/you/docker-playground
Then add the Docker client library as a dependency:
go get github.com/docker/docker/client
In your main.go, start by importing the Docker client and creating a client instance:
import (
"context"
"github.com/docker/docker/client"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
if err != nil {
panic(err)
}
defer cli.Close()
// Your code goes here...
}
FromEnv reads DOCKER_HOST, DOCKER_API_VERSION, etc.WithAPIVersionNegotiation ensures your code talks the right API version for your engine.The snippet retrieves all containers (running or stopped) and groups them by their Compose service:
// List all containers
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
// Group into a map[serviceName][]container.Summary
composeServices := groupContainersByComposeService(containers)
Under the hood, ContainerList maps to docker container ls --all. You can reproduce it:
docker container ls --all --format "{{json .}}"
The helper function simply reads the com.docker.compose.service label:
func groupContainersByComposeService(cs []container.Summary) map[string][]container.Summary {
m := map[string][]container.Summary{}
for _, c := range cs {
s := c.Labels["com.docker.compose.service"]
if s == "" {
s = "non-compose"
}
m[s] = append(m[s], c)
}
return m
}
This is equivalent to filtering with:
docker ps -a --filter "label=com.docker.compose.service=myservice"
Once grouped, you can iterate in sorted order and print out:
for service, ctList := range sortedServices {
fmt.Printf("Service %s: %d containers\n", service, len(ctList))
// show labels and details
}
This mirrors:
docker inspect --format '{{ json .Config.Labels }}' <containerID>
Next, we demonstrate how to pull an image and launch a lightweight Alpine container:
reader, err := cli.ImagePull(ctx, "alpine", image.PullOptions{})
io.Copy(os.Stdout, reader)
resp, _ := cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"sleep", "60"},
Tty: true,
}, nil, nil, nil, "alpine-demo")
cli.ContainerStart(ctx, resp.ID, container.StartOptions{})
In your shell, this equates to:
docker pull alpine
# create a sleeping container
docker run -d --name alpine-demo alpine sleep 60
You can exec into a running container programmatically:
execConfig := container.ExecOptions{ Cmd: []string{"ls", "-la", "/"}, AttachStdout: true }
execID, _ := cli.ContainerExecCreate(ctx, containerID, execConfig)
attachResp, _ := cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
stdcopy.StdCopy(os.Stdout, os.Stderr, attachResp.Reader)
Which maps directly to CLI:
docker exec alpine-demo ls -la /
Repeat for system info or file operations:
docker exec alpine-demo sh -c "cat /etc/os-release && uname -a"
and
docker exec alpine-demo sh -c "echo 'Hello!' > /tmp/hello.txt && cat /tmp/hello.txt"
After testing, the Go code stops and removes the container:
cli.ContainerStop(ctx, containerID, container.StopOptions{})
cli.ContainerRemove(ctx, containerID, container.RemoveOptions{})
Or via shell:
docker stop alpine-demo
docker rm alpine-demo
Below is the full code, combining all the steps above into a single executable:
package main
import (
"context"
"fmt"
"io"
"os"
"sort"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
defer cli.Close()
// List all containers
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
panic(err)
}
// Group containers by compose service
composeServices := groupContainersByComposeService(containers)
// Print the containers grouped by service
fmt.Println("Containers")
fmt.Println("=====================================")
// Sort service names for consistent output
var serviceNames []string
for service := range composeServices {
serviceNames = append(serviceNames, service)
}
sort.Strings(serviceNames)
for _, service := range serviceNames {
containers := composeServices[service]
fmt.Printf("\nService: %s (%d containers)\n", service, len(containers))
fmt.Println("-----------------------------------")
if service != "non-compose" && len(containers) > 0 {
if path, exists := containers[0].Labels["com.docker.compose.project.working_dir"]; exists {
fmt.Printf(" Project path: %s\n", path)
}
if configFiles, exists := containers[0].Labels["com.docker.compose.project.config_files"]; exists {
fmt.Printf(" Config files: %s\n", configFiles)
}
}
for _, c := range containers {
fmt.Printf(" - %s (ID: %.12s)\n", c.Names[0], c.ID)
fmt.Printf(" Status: %s\n", c.Status)
fmt.Printf(" Image: %s\n", c.Image)
}
}
fmt.Println()
fmt.Println()
// Pull Alpine image
fmt.Println("Pulling Alpine image...")
reader, err := cli.ImagePull(ctx, "docker.io/library/alpine", image.PullOptions{})
if err != nil {
panic(err)
}
io.Copy(os.Stdout, reader)
containerName := "alpine-demo"
fmt.Println("\nCreating container:", containerName)
resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"sleep", "60"},
Tty: true,
}, nil, nil, nil, containerName)
if err != nil {
panic(err)
}
containerID := resp.ID
fmt.Printf("Container created with ID: %.12s\n", containerID)
fmt.Println("Starting container...")
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
panic(err)
}
fmt.Println("Container started successfully")
fmt.Println("\nExecuting commands in the running container:")
fmt.Println("\n1. Directory listing (ls -la /)")
fmt.Println("=====================================")
execConfig := container.ExecOptions{
Cmd: []string{"ls", "-la", "/"},
AttachStdout: true,
AttachStderr: true,
Tty: false,
}
execID, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
if err != nil {
panic(err)
}
execAttachResp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
panic(err)
}
defer execAttachResp.Close()
stdcopy.StdCopy(os.Stdout, os.Stderr, execAttachResp.Reader)
fmt.Println("\n2. System Information")
fmt.Println("=====================================")
execConfig = container.ExecOptions{
Cmd: []string{"sh", "-c", "cat /etc/os-release && echo '' && uname -a"},
AttachStdout: true,
AttachStderr: true,
Tty: false,
}
execID, err = cli.ContainerExecCreate(ctx, containerID, execConfig)
if err != nil {
panic(err)
}
execAttachResp, err = cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
panic(err)
}
defer execAttachResp.Close()
stdcopy.StdCopy(os.Stdout, os.Stderr, execAttachResp.Reader)
fmt.Println("\n3. Creating and reading a file")
fmt.Println("=====================================")
execConfig = container.ExecOptions{
Cmd: []string{"sh", "-c", "echo 'Hello from Alpine container!' > /tmp/hello.txt && cat /tmp/hello.txt"},
AttachStdout: true,
AttachStderr: true,
Tty: false,
}
execID, err = cli.ContainerExecCreate(ctx, containerID, execConfig)
if err != nil {
panic(err)
}
execAttachResp, err = cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
panic(err)
}
defer execAttachResp.Close()
stdcopy.StdCopy(os.Stdout, os.Stderr, execAttachResp.Reader)
fmt.Println("\nStopping and removing container...")
err = cli.ContainerStop(ctx, containerID, container.StopOptions{})
if err != nil {
panic(err)
}
err = cli.ContainerRemove(ctx, containerID, container.RemoveOptions{})
if err != nil {
panic(err)
}
fmt.Println("Container stopped and removed successfully")
}
func groupContainersByComposeService(containers []container.Summary) map[string][]container.Summary {
serviceMap := make(map[string][]container.Summary)
for _, c := range containers {
serviceName, exists := c.Labels["com.docker.compose.service"]
if !exists {
serviceName = "non-compose"
}
serviceMap[serviceName] = append(serviceMap[serviceName], c)
}
return serviceMap
}