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.