Building Docker Images Dynamically with Go

how to build Docker images dynamically using the Go Docker client.

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

func createBuildContext() (io.Reader, error) {
  // get current working dir
  wd, err := os.Getwd()
  if err != nil {
    return nil, err
  }
  // resolve Dockerfile path
  path := filepath.join(wd, "Dockerfile")
  return os.Open(path)
}

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 .

import "github.com/docker/docker/pkg/archive"

// createBuildContext archive a dir and return an io.Reader
func createBuildContext(path string) (io.Reader, error) {
	return archive.Tar(path, archive.Uncompressed)
}

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

// buildLocalImage build a docker image from the supplied `path` parameter.
// The image built is intended to be pushed to a local docker registry.
// This function assumes there is a Dockerfile in the dir
func buildLocalImage(path string) error {
  // get current working dir, to resolve the path to Dockerfile
  wd, err := os.Getwd()
  if err != nil {
     return err
  }

  // create a docker buildContext by `archiving` the files
  // the target dir
  buildCtx, err := createBuildContext(path)
  if err != nil {
     return err
  }

  // form a unique docker tag. the first string seg is the local docker registry host
  tag := fmt.Sprintf("%s%s%s", "docker-registry:5000/", build.Name(), p.md5()[:6])
  ctx := context.Background()
  // build image. reader can be used to get output from docker deamon
  reader, err := p.client.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{
     Dockerfile: "Dockerfile", PullParent: true, Tags: []string{tag}, Remove: true, NoCache: true,
  })
  if err != nil {
     return err
  }

  for {
	buf := make([]byte, 512)
	_, err := reader.Body.Read(buf)
	if err != nil {
	   if err == io.EOF {
		break
	    }
	    log.Println("error reading response ", err)
	    continue
	}
	// print outputs
  	log.Println(string(buf[:]))
   }
  // yay! no errors
  return nil
}

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

Thank you.