Building Docker Images Dynamically with Go

I recently started looking into ways of automating microservices app deployment and one of the many things i needed to automate is the famous docker build command. I understand that i could take advantage of the installed Docker client on the host computer by using os/exec package, but my idea isn’t that simple and its not really fun compared to using github.com/docker/docker/client — refer to as goDockerClient henceforth. This post contains the steps i followed to building docker images successfully with goDockerClient

Understand the Docker BuildContext

After i spent some time checking out goDockerClient GoDoc, i felt like i was ready to start building docker images dynamically but i was wrong. It wasn’t as trivial as the Doc made it look, i thought all i had to do was call client.ImageBuild(context.Context, buildContext, opts) specifying my Dockerfile in opts.Dockerfile , after few unsuccessful trials, i began digging deeper. Turns out buildContext which is of type io.Reader is suppose to be the content of the image i am trying to build. Initially, i was doing something like this

 1func createBuildContext() (io.Reader, error) {
 2  // get current working dir
 3  wd, err := os.Getwd()
 4  if err != nil {
 5    return nil, err
 6  }
 7  // resolve Dockerfile path
 8  path := filepath.join(wd, "Dockerfile")
 9  return os.Open(path)
10}

Using just the Dockerfile as buildContext will not work because the docker daemon expect the buildContext to be all the files you’ll need in your new docker image.

What worked

After understanding what docker meant by buildContext the task at hand became easier. We just need a way to wrap all the files in a dir — BuildContext into an io.Reader so that we can easily send this to docker deamon and have our image built. Luckily, there is a helper function in goDockerClient that does just this, just give it a directory and this function would tar it and give you an io.Reader .

1import "github.com/docker/docker/pkg/archive"
2
3// createBuildContext archive a dir and return an io.Reader
4func createBuildContext(path string) (io.Reader, error) {
5	return archive.Tar(path, archive.Uncompressed)
6}

The final solution. The code below results to a successful dynamic docker build

 1// buildLocalImage build a docker image from the supplied `path` parameter.
 2// The image built is intended to be pushed to a local docker registry.
 3// This function assumes there is a Dockerfile in the dir
 4func buildLocalImage(path string) error {
 5  // get current working dir, to resolve the path to Dockerfile
 6  wd, err := os.Getwd()
 7  if err != nil {
 8     return err
 9  }
10
11  // create a docker buildContext by `archiving` the files
12  // the target dir
13  buildCtx, err := createBuildContext(path)
14  if err != nil {
15     return err
16  }
17
18  // form a unique docker tag. the first string seg is the local docker registry host
19  tag := fmt.Sprintf("%s%s%s", "docker-registry:5000/", build.Name(), p.md5()[:6])
20  ctx := context.Background()
21  // build image. reader can be used to get output from docker deamon
22  reader, err := p.client.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{
23     Dockerfile: "Dockerfile", PullParent: true, Tags: []string{tag}, Remove: true, NoCache: true,
24  })
25  if err != nil {
26     return err
27  }
28
29  for {
30	buf := make([]byte, 512)
31	_, err := reader.Body.Read(buf)
32	if err != nil {
33	   if err == io.EOF {
34		break
35	    }
36	    log.Println("error reading response ", err)
37	    continue
38	}
39	// print outputs
40  	log.Println(string(buf[:]))
41   }
42  // yay! no errors
43  return nil
44}

Full code gist can be found here — https://gist.github.com/adigunhammedolalekan/354f31e7f9b53e6c76d09b2247d3ecad

Thank you.