Dockerising a GoLang Application

I was recently messing about with GoLang to get my head around it, and came up with a small script to basically output a timestamp and a random name, based on the Docker container random name generator (here). The plan was to use this to populate a database with random junk for when I’m testing software against database servers.

I got the code working anyway, you can see it on my GitHub site here, in this article I’m going to focus on the differences between building a container from a GoLang binary, and the benefits of doing this from scratch. This is to my mind a very powerful way of delivering Go applications so that they are both tiny and self-contained, and sets it apart from other popular languages where they end up dragging round baggage in their dependencies.

The steps below will go through building a container from a Go file, first using the GoLang image from Docker Hub, and then from scratch, and we will compare the resulting sizes of the files.

The steps below were all carried out on macOS 10.12, Go 1.8, and Docker 17.03.

Image based on golang official Docker image

For this stage, we should have our Go file in the current folder, we need to create the following Dockerfile:

FROM golang:latest
RUN mkdir /app
ADD <path_to_go_file>.go /app/
RUN go build -o <output_file> .
CMD ["/app/<output_file>"]

We can then build this into an image:

docker build -t <your_name>/<image_name> -f Dockerfile .

We can then use ‘docker images’ to show the image we created:

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
timhynes/name_gen_full   latest              ffc0ef4bac73        3 seconds ago       698 MB

Image built from scratch with Go binary

To build the image from scratch, we first need to build our Go application into a binary. We do this with the below command:

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o <output_file> <path_to_go_file> 

The ‘GOOS’ flag defines what the target OS for the binary is, in our case we choose Linux so that it will run in our Docker container. The ‘CGO_ENABLED=0’ flag prevents linking to external C libraries (more info here), and will mean that the binary is fully self-contained.

Once this is run, the binary will be created in the location specified. This could then be ported around Linux systems and run as a compiled application, but we are going to build this into a Docker image instead.

To build the Docker image, as shown earlier, we need a Dockerfile; the one I used for this stage is shown below:

FROM scratch
ADD <output_file> /
CMD ["<output_file>"]

This should be saved to the same folder as the binary as ‘Dockerfile’. We can now run the ‘docker build’ command to create our image:

docker build -t <your_name>/<image_name> -f Dockerfile .

At this point we should now have an image in our local repository, this shows here as being under 2MB for my application:

REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
timhynes/name_gen     latest              436a832943c8        12 seconds ago      1.77 MB


This shows that despite the functional endpoint being ultimately to containerise our application, through compiling a GoLang binary, and using this as the sole contents of our image, we can save huge amounts of space. In the case of the example above, the resultant image was over 300 times smaller when using the binary alone.

I guess the takeaway is that not all containers are made equal, and thinking about the way we package our Docker application can make a large difference to our ability to be able to deliver and run it.