Over the past year we've written a few articles about optimizing your Docker images -- usually with an emphasis on creating the smallest possible image. Unfortunately, when using an interpreted language like Ruby (which has been used for many of the CenturyLink projects) there is only so much fat you can trim from your images. To run a Ruby application in a container you still need the Ruby interpreter and a whole host of OS packages and Ruby Gems installed. Even with our best effort to optimize the images, our Docker-packaged Ruby applications often weigh-in at 400+ MBs.

Go

We've now started to use Go for some of our latest projects which, among other things, gives us the ability to package our apps into really compact images. With a Go application, you can compile your code into a self-contained, statically-linked binary that has absolutely no external dependencies. Your application code along with the Go runtime and any imported packages are all compiled together into one binary.

The primary benefit of a statically-linked binary is that it allows you to deploy your application by simply copying the binary. When deploying with Docker this means you can build an image for your application that contains nothing but the app itself. You can go from using ubuntu (130 MB) or debian (85 MB) as your base image to something like busybox (2 MB) or even scratch (0 bytes).

If you take your 4 MB, statically-linked Go binary and package it in a Docker container using scratch as your base you'll end-up with a 4 MB image -- that's 1/100th the size of our packaged Ruby app!

golang-builder

After playing with a few Go applications and reading Adriaan de Jonge's blog post "Create the Smallest Possible Docker Container", we thought it would be interesting to see if we could create a general purpose tool that could take any Go project and turn it into a slim image. The result of that work is golang-builder.

golang-builder is itself a containerized app that can compile Go code into a static binary and package it in a Docker image.

Example

Let's say we have a basic "Hello World" Go application:

package main // import "github.com/CenturyLinkLabs/hello"

import "fmt"

func main() {
    fmt.Println("Hello World")
}

and a corresponding Dockerfile:

FROM scratch
COPY hello /
ENTRYPOINT ["/hello"]

To package this application in a Docker image, we would invoke the golang-builder as follows:

docker run --rm \
    -v $(pwd):/src \
    -v /var/run/docker.sock:/var/run/docker.sock \
    centurylink/golang-builder

Assuming that we're currently in the directory containing our hello.go source file and Dockerfile, the first -v flag will mount our project directory into the golang-builder container as /src. The script inside golang-builder is hard-coded to look for your Go code at /src.

The second -v flag mounts the Docker API socket into the container. Since the golang-builder code needs to interact with the Docker API in order to build the final image, it needs access to /var/run/docker.sock.

The golang-builder will set-up the appropriate GOPATH for your project, resolve your dependencies, compile your code and then issue a docker build against your project's Dockerfile.

The end result should be a new image in your Docker image list:

$ docker images
REPOSITORY   TAG      IMAGE ID       CREATED          VIRTUAL SIZE
hello        latest   43bc9ae1080a   10 seconds ago   1.356 MB

If you're only interested in the statically-linked binary, you can omit the volume mount for the Docker socket and golang-builder will stop after compiling your application:

docker run --rm -v $(pwd):/src centurylink/golang-builder

If you're interested in learning more take a look at the golang-builder page on the Docker Hub or check out the source on GitHub.