Docker and Go mascot illustration
by Ashley McNamara
Introduction
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:
Prerequisites
- Go installed (v1.18+ recommended)
- Docker Engine running locally (desktop or daemon)
- A terminal on macOS, Linux, or WSL2 (Windows)
1. Bootstrapping your Go module
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
2. Connecting to Docker from Go
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
readsDOCKER_HOST
,DOCKER_API_VERSION
, etc.WithAPIVersionNegotiation
ensures your code talks the right API version for your engine.
3. Listing and grouping containers
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"
4. Printing service summaries
Once grouped, you can iterate in sorted order and print out:
- Service name
- Number of containers
- Compose project path & config files (via labels)
- Container name, status, and image
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>
5. Pulling and running an Alpine container
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
6. Executing commands inside the container
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"
7. Cleanup
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
Full Code
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("-----------------------------------")
// Get the project filepath and config files if they exist (should be same for all containers in a service)
if service != "non-compose" && len(containers) > 0 {
// Get project working directory
if path, exists := containers[0].Labels["com.docker.compose.project.working_dir"]; exists {
fmt.Printf(" Project path: %s\n", path)
}
// Get compose config files
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)
// Create container with Alpine image - just create with a sleep command
// so it stays alive for us to execute commands in it
containerName := "alpine-demo"
fmt.Println("\nCreating container:", containerName)
resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"sleep", "60"}, // Container will stay alive for 60 seconds
Tty: true,
}, nil, nil, nil, containerName)
if err != nil {
panic(err)
}
containerID := resp.ID
fmt.Printf("Container created with ID: %.12s\n", containerID)
// Start container
fmt.Println("Starting container...")
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
panic(err)
}
fmt.Println("Container started successfully")
// Now let's execute various commands in the running container
fmt.Println("\nExecuting commands in the running container:")
// Execute ls -la / command
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)
}
// Attach to the exec instance to get its output
execAttachResp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
panic(err)
}
defer execAttachResp.Close()
// Copy the output to stdout
stdcopy.StdCopy(os.Stdout, os.Stderr, execAttachResp.Reader)
// Execute command to show system info
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)
}
// Attach to the exec instance
execAttachResp, err = cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
panic(err)
}
defer execAttachResp.Close()
// Copy the output to stdout
stdcopy.StdCopy(os.Stdout, os.Stderr, execAttachResp.Reader)
// Execute a command to create a file and then read it
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)
}
// Attach to the exec instance
execAttachResp, err = cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
panic(err)
}
defer execAttachResp.Close()
// Copy the output to stdout
stdcopy.StdCopy(os.Stdout, os.Stderr, execAttachResp.Reader)
// Stop and remove the container when we're done
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")
}
// groupContainersByComposeService groups containers by their Docker Compose service name
func groupContainersByComposeService(containers []container.Summary) map[string][]container.Summary {
serviceMap := make(map[string][]container.Summary)
for _, c := range containers {
// Look for the Docker Compose service label
serviceName, exists := c.Labels["com.docker.compose.service"]
if !exists {
// If the container isn't part of a compose service, group it under "non-compose"
serviceName = "non-compose"
}
serviceMap[serviceName] = append(serviceMap[serviceName], c)
}
return serviceMap
}