Controlling the Docker Engine from Go

Today most of the production systems run on containers. And most of the containerization is done through docker. Docker solves one of the biggest problem we face in software development:

¯\_(ツ)_/¯ 
Works on my machine

We use the docker command to talk to the docker daemon running in our host machine.

$ docker run nginx
$ docker ps
$ docker stop <container_id>

The code behind the docker command is the docker cli. It uses docker client library to communicate with the docker daemon through the docker engine REST API.

Yes the docker command we use day to day is API driven, which means we can wire quick solutions to scale up the docker apps. Lets’s see how we can achieve the basic docker actions like run, list and remove through Golang.

You can install the docker SDK for go using go get.

go get github.com/docker/docker

Here’s what we are going to do, we will take nginx:latest image and do some basic actions. The below sections are going to have the commands, the respective code snippet and explanation for the code.

Create docker container

Command

$ docker run nginx:latest

Code

All the Docker Engine APIs are available through the cli object returned by client.NewEnvClient().

cli, err := client.NewEnvClient()
if err != nil {      
fmt.Println("Unable to create docker client")
panic(err)
}

The NewEnvClient returns the client by taking in required options from the environment variables instead of function parameters.

No need to worry about the environment variables for now, unless you need to pass in some options. By default it will take care of it. We use client.NewEnvClient() because it’s the simplest way to create docker client without passing any arguments.

cli.ContainerCreate creates the container for us. It takes in several arguments and returns aContainerCreateCreatedBody struct with container ID in it. The type signature of the function looks like this:

ContainerCreate func(ctx context.Context, 
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string) (container.ContainerCreateCreatedBody, error)

The parameters, config, hostConfig and networkingConfig are the types that needs to be imported from docker module.

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"

After getting to know the function parameters we call the function cli.ContainerCreate()

cont, err := cli.ContainerCreate( 
context.Background(),
&container.Config{
Image: image, },
&container.HostConfig {
PortBindings: portBinding, }, nil, "")

if err != nil {
panic(err)
}

Since we are running nginx container, we have to expose port 80 of the container to the port 8000 of host machine. The mapping is passed as an attribute: PortBindings to the hostConfig parameter struct. Note that we already built the mapping from importing it’s types from the package nat.

hostBinding := nat.PortBinding{ 
HostIP: "0.0.0.0",
HostPort: "8000",
}
containerPort, err := nat.NewPort("tcp", "80") 
if err != nil{
panic("Unable to get the port")
}

After creating the container, we get the container ID from returned object and start the container.

cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})

If you wrap this code into a package and call the function, the address localhost:8000 on your browser should print a welcome message from nginx that’s running inside the container.

nginx welcome message on localhost:8000

List docker containers

Command

$ docker container ps

Code

The code here is fairly simple, you create a NewEnvClient(), call the ContainerList and then iterate through the returned list of containers. ContainerList takes in several options such as limit, filter etc. of the type types.ContainerListOptions, but we just pass in empty options.

Stop docker container

Command

$ docker container stop <container-id>

Code

The ContainerStop() also takes in a timeout as it’s last argument. Here we are not specifying any timeouts to stop the container.

err = cli.ContainerStop(context.Background(), containerID, nil)
if err != nil {  
panic(err)
}

Taking it to production

The code you saw is almost production ready:)
You can take this code to production by just wrapping those snippets into it’s own packages. Replace all panic with return statements and replace Print statements with some logging.

Here you go, I have already done that for you.

Clone the repository and cd to docker-sdk folder. The code structure there should look like this:

docker-sdk
|
|_ main.go
|
|_ glide.lock
|
|_ glide.yaml
|
|_ container/
|
|_ container.go
|
|_ container_test.go

I have used Glide as a package manager here. So, install glide by doing brew install glide. If you’re not on macOS, do go get github.com/Masterminds/glide.

Install the dependencies by running glide install from the docker-sdk folder.

Now you can play with the code by calling different functions of the container package inside main.go.


This post originally appeared in Backend Army.

Was the post useful to you? 
Hold the clap button and give a shoutout to me on twitter. ❤️