Compile and Run Golang Executable with Docker

In this tutorial, I guide you through the process of writing a Dockerfile which compiles and runs a Golang application packaged into a Docker image. Thereby, I use a multi-stage setup, removing the need for seperate Dockerfiles for compilation and execution.

For this article, I am using my recently published csv2gpx Golang application for geocaching.com. It consists of a main.go file, the entrypoint for the compilation. The Kingpin library parses the command line arguments. The converter class is called with these arguments and converts the given CSV file into a GPX file for geocaching.com

Some Docker basics

A Dockfiler describes the structure of a Docker image. It defines the commands to build the exact same image every time it is built on any machine. Each line of the Dockerfile contains one command in the form

Each command creates a new layer in the resulting image. But only the last layer is writable. All others are read-only.

The docker build command builds an image using the Dockerfile. The example shows a build command for the csv2gpx application mentioned above. In this example, nothing has changed in the second stage (step 10 to 14) compared to the previous execution. Therefore, Docker utilises the information it has from its cache.

The –rm option removes intermediate containers after a successful build. The -t option names and optionally adds a tag in the ‘name:tag’ format to the image. By default, it adds the tag latest to the image name. The dot at the end defines that the current working directory is used as build context.

In the following, I will go through the Dockerfile command by command until we finally end up with the complete file. I am using a multi-stage build, a feature introduced with Docker 17.05. It greatly simplifies keeping the final image small by defining several stages, where each FROM instruction can use a different base.

First stage: Let’s go

The first command is the FROM which defines the base image. We want to construct a golang image, thus we use one of the base images from the Golang Docker hub.

Here, I am saying that any base image built on alpine with major version 1 is fine (1.11.5, 1.12.0, etc.). Alpine is only 5MB big, but comes with a shell (sh, not bash).

Next, I use the LABEL instruction to define the maintainer of this package (The MAINTAINER command is deprecated). It adds the information as metadata to the image .

Each of the Docker commands is by default executed in the root directory. If we want to change this, we can use the WORKDIR command. This command also generates directories recursively in case they do not exist.

Here, I am following the standard Go project folder structure convention.

Download dependencies

Next, we have to download the libraries used in our Golang application. I am only relying on the Kingpin command line parser. First, we have to download Git, then we can let Docker download the library from Github.

Package manage apk is the tool used to install, upgrade, or delete libraries in the image. The –update flag fetches the current package index before adding the package.

Copy data into the container

Next, we copy the Go code and necessary data files into the image.

COPY main.go . copies the main.go file directly into the WORKDIR, whereas COPY data/example_input.csv ./data/ copies the csv file into a subdirectory named data, which is created on the fly.

Build the app

Now, we can build the Go app with the Go compiler using the RUN command.

We are disabling cgo which gives us a static binary with all libraries built in. We set GOOS and GOARCH to define the compilation target (you could also cross-compile it as a Windows executable). By setting -o app we are naming the resulting binary.

Second stage: Runnable image

The second FROM instruction starts a new build stage with the alpine image as its base. The COPY –from=builder line copies just the built artifact from the builder stage into this new stage and leaves everything else behind.

Next, we define two environment variables, INPUT and OUTPUT, which are set at runtime in the go run command.

The ENV instruction sets the environment variable key to a value. This value will be in the environment for all subsequent instructions. Here, both enviroment variables are initialised as empty string. They are set using the -e flag of the docker run command.

Finally, the CMD instruction defines the command to be executed when running the image. There can only be one CMD instruction (if there are more, only the last one is considered).

Due to this issue, I am using the shell form. Thereby, the command is executed in /bin/sh -c and the ENV variables are resolved to their values.

Summary

To sum it up, here is the full Dockerfile

and the docker build and docker run command.

where the later one produces for example the following output