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

COMMAND parameter(s)

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.

C:\Users\Matthias\go\src\github.com\matthiassommer\csv2gpx>docker build --rm -t csv2gpx .

Sending build context to Docker daemon  23.58MB

Step 1/14 : FROM golang:1-alpine AS builder
 ---> cb1c8647572c
Step 2/14 : LABEL maintainer="Matthias Sommer, matthiassommer@posteo.de"
 ---> Running in 983e57bab04a
Removing intermediate container 983e57bab04a
 ---> 878f934dcb4e
Step 3/14 : WORKDIR /go/src/github.com/matthiassommer/csv2gpx
 ---> Running in 3b86ce182668
Removing intermediate container 3b86ce182668
 ---> d5b6c98b3364
Step 4/14 : RUN apk add --update git
 ---> Running in 1e3b9455755d
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/6) Installing nghttp2-libs (1.35.1-r0)
(2/6) Installing libssh2 (1.8.0-r4)
(3/6) Installing libcurl (7.64.0-r1)
(4/6) Installing expat (2.2.6-r0)
(5/6) Installing pcre2 (10.32-r1)
(6/6) Installing git (2.20.1-r0)
Executing busybox-1.29.3-r10.trigger
OK: 20 MiB in 21 packages
Removing intermediate container 1e3b9455755d
 ---> 2cd7a342f98e
Step 5/14 : RUN go get gopkg.in/alecthomas/kingpin.v2
 ---> Running in 547f25084238
Removing intermediate container 547f25084238
 ---> 52baca484098
Step 6/14 : COPY data/example_input.csv ./data/
 ---> d89836342bc6
Step 7/14 : COPY main.go .
 ---> b2700b154032
Step 8/14 : COPY converter.go .
 ---> 1e0ddd684bf3
Step 9/14 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o app .
 ---> Running in 959e46c2120f
Removing intermediate container 959e46c2120f
 ---> 515a6cb5ad31

Step 10/14 : FROM alpine
 ---> caf27325b298
Step 11/14 : COPY --from=builder /go/src/github.com/matthiassommer/csv2gpx/ .
 ---> Using cache
 ---> 2c98343ee8be
Step 12/14 : ENV INPUT ""
 ---> Using cache
 ---> c45a03d23f5b
Step 13/14 : ENV OUTPUT ""
 ---> Using cache
 ---> 3631bd10479b
Step 14/14 : CMD ./app $INPUT $OUTPUT
 ---> Using cache
 ---> d93fdf209c5c

Successfully built d93fdf209c5c
Successfully tagged csv2gpx:latest

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.

FROM golang:1-alpine AS builder

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 .

LABEL maintainer="Matthias Sommer, matthiassommer@posteo.de"

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.

WORKDIR /go/src/github.com/matthiassommer/csv2gpx

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.

RUN apk add --update git
RUN go get gopkg.in/alecthomas/kingpin.v2

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 data/example_input.csv ./data/
COPY main.go .
COPY converter.go .

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.

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o app .

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.

ENV INPUT ""
ENV OUTPUT ""

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.

docker run -e INPUT=example_input.csv -e OUTPUT=example_input.gpx csv2gpx

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).

CMD ./app $INPUT $OUTPUT

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

FROM golang:1-alpine AS builder

LABEL maintainer="Matthias Sommer, matthiassommer@posteo.de"

WORKDIR /go/src/github.com/matthiassommer/csv2gpx

# install dependencies
RUN apk add --update git
RUN go get gopkg.in/alecthomas/kingpin.v2

# copy the code and data
COPY data/example_input.csv ./data/
COPY main.go .
COPY converter.go .

# build the app
RUN GOOS=linux GOARCH=amd64 go build -o app .

# Second stage
FROM alpine

COPY --from=builder /go/src/github.com/matthiassommer/csv2gpx/ .

ENV INPUT ""
ENV OUTPUT ""

CMD ./app $INPUT $OUTPUT

and the docker build and docker run command.

docker build --rm -t csv2gpx .
docker run -e INPUT=data/example_input.csv -e OUTPUT=data/example_input.gpx csv2gpx

where the later one produces for example the following output

CSV: data/example_input.csv GPX: data/example_input.gpx
Total rows: 2
Converted rows: 2
GPX written to data/example_input.gpx