Imagine you want to compile a Go application, but you cannot do it locally. It might be that you want to cross-compile but you need CGO. Maybe you just want to test a new version of Go for compiling before taking the jump?

In this post, we'll show a way how to do this by using a Docker container.

The first step is to build a base image to start from. This base image can contain all the packages you need in addition to Go itself. For doing so, we start from the golang:1.11-alpine3.8 base image and build upon that. If prefer the Alpine version of the golang image as that's a lot smaller than the golang:1.11 image.

The Dockerfile.base looks as follows:

FROM golang:1.11-alpine3.8

RUN apk update && apk add gcc libc-dev make git

This uses Go 1.11 and additionally installs gcc, the libc headers, make and git. If you want to build starting from this image (to avoid that you always have to rebuild the image from scratch), you can build it using the following command:

$ docker build --rm -t custom-go1.11:latest -f Docker.base .

This will output the following:

Sending build context to Docker daemon  335.2MB
Step 1/2 : FROM golang:1.11-alpine3.8
 ---> 20ff4d6283c0
Step 2/2 : RUN apk update && apk add gcc libc-dev make git
 ---> Running in a2a95ff75ab6
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz
v3.8.0-94-gdea4c10014 [http://dl-cdn.alpinelinux.org/alpine/v3.8/main]
v3.8.0-92-g87a3f3ec11 [http://dl-cdn.alpinelinux.org/alpine/v3.8/community]
OK: 9542 distinct packages available
(1/20) Installing binutils (2.30-r5)
(2/20) Installing gmp (6.1.2-r1)
(3/20) Installing isl (0.18-r0)
(4/20) Installing libgomp (6.4.0-r8)
(5/20) Installing libatomic (6.4.0-r8)
(6/20) Installing pkgconf (1.5.3-r0)
(7/20) Installing libgcc (6.4.0-r8)
(8/20) Installing mpfr3 (3.1.5-r1)
(9/20) Installing mpc1 (1.0.3-r1)
(10/20) Installing libstdc++ (6.4.0-r8)
(11/20) Installing gcc (6.4.0-r8)
(12/20) Installing nghttp2-libs (1.32.0-r0)
(13/20) Installing libssh2 (1.8.0-r3)
(14/20) Installing libcurl (7.61.0-r0)
(15/20) Installing expat (2.2.5-r0)
(16/20) Installing pcre2 (10.31-r0)
(17/20) Installing git (2.18.0-r0)
(18/20) Installing musl-dev (1.1.19-r10)
(19/20) Installing libc-dev (0.7.1-r0)
(20/20) Installing make (4.2.1-r2)
Executing busybox-1.28.4-r0.trigger
OK: 114 MiB in 34 packages
Removing intermediate container a2a95ff75ab6
 ---> 15cde1650813
Successfully built 15cde1650813
Successfully tagged custom-go1.11:latest

You can then use the following command to verify that the image was built:

$ docker images list
REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
custom-go1.11                   latest              15cde1650813        14 minutes ago      423MB

Now that the image is built, you can use that for compiling your Go app. Instead of creating a separate Dockerfile which is then also used to run the app, we are going to use the docker run command instead:

$ docker run --rm -v `pwd`:/go -w="/go" --ldflags '-extldflags "-static"' -e GOARCH=amd64 -e GOOS=linux custom-go1.11 make build

Let's decompose the command and see what each argument does:

  • docker run: tells docker you want to run something inside a container
  • --rm: causes the container to be removed automatically after it exits
  • -v `pwd`:/go: this maps the current directory to the /go path inside the container. This should be your $GOPATH if possible or at least contain a src folder.
  • -w="/go": sets the working directory in the container to /go
  • --ldflags '-extldflags "-static"': compiles the app as a static binary, avoiding problems with which libc library is linked (the one on Alpine is different than the one on Ubuntu for example)
  • -e GOARCH=amd64: sets the GOARCH environment variable to amd64
  • -e GOOS=linux: sets the GOOS environment variable to linux
  • custom-go1.11: we are using the custom-go1.11 image to run the command in
  • make build: in the /go path, the make target called build will be run.

This will build the binary using the container and depending on where you place the binary, it will be on the host machine.

You can also use this technique if you don't want to install Go on your machine itself, but that's not really the advised way of working.

Inspired by the DOCKER + GOLANG = ❤️ post on the Docker blog.