This is a short collection of tips and tricks showing how Docker can be useful when working with Go code. For instance, I’ll show you how to compile Go code with different versions of the Go toolchain, how to cross-compile to a different platform (and test the result!), or how to produce really small container images.

The following article assumes that you have Docker installed on your system. It doesn’t have to be a recent version (we’re not going to use any fancy feature here).

Go without go

go

If you write Go code, or if you have even the slightest interest into the Go language, you certainly have the Go compiler and toolchain installed, so you might be wondering “what’s the point?”; but there are a few scenarios where you want to compile Go without installing Go.

  • You still have this old Go 1.2 on your machine (that you can’t or won’t upgrade), and you have to work on this codebase that requires a newer version of the toolchain.
  • You want to play with cross compilation features of Go 1.5 (for instance, to make sure that you can create OS X binaries from a Linux system).
  • You want to have multiple versions of Go side-by-side, but don’t want to completely litter your system.
  • You want to be 100% sure that your project and all its dependencies download, build, and run fine on a clean system.

If any of this is relevant to you, then let’s call Docker to the rescue!

Compiling a program in a container

go get -v github.com/user/repo-v
go get github.com/user/repo/...

We can do that in a container!

Try this:

docker run golang go get -v github.com/golang/example/hello/...

This will pull the golang image (unless you have it already; then it will start right away), and create a container based on that image. In that container, go will download a little “hello world” example, build it, and install it. But it will install it in the container … So how do we run that program now?

Running our program in a container

One solution is to commit the container that we just built, i.e. “freeze” it into a new image:

docker commit $(docker ps -lq) awesomeness
docker ps -lq

Now, we can run our program in a container based on the image that we just built:

docker run awesomeness hello
Hello, Go examples!

Bonus points

docker commit--changeCMDENTRYPOINTdocker run awesomeness

Running in a throwaway container

What if we don’t want to create an extra image just to run this Go program?

We got you covered:

docker run --rm golang sh -c \
"go get github.com/golang/example/hello/... && exec hello"

Wait a minute, what are all those bells and whistles?

--rmdocker rmgo getexec hello&&&&go get...exec hello)andtruesh -cdocker run golang "go get ... && hello"go SPACE get SPACE etcexec hellohellohellohellohellodocker stopSIGTERM

Using a different version of Go

golang golang:latest,

If you want to use a specific version of Go, that’s very easy: specify that version as a tag after the image name.

golanggolang:1.5
docker run --rm golang:1.5 sh -c \
 "go get github.com/golang/example/hello/... && exec hello"

You can see all the versions (and variants) available on the Golang image page on the Docker Hub.

Installing on our system

OK, so what if we want to run the compiled program on our system, instead of in a container?

We could copy the compiled binary out of the container. Note, however, that this will work only if our container architecture matches our host architecture; in other words, if we run Docker on Linux. (I’m leaving out people who might be running Windows Containers!)

$GOPATH/bingolang$GOPATH/go.
docker run -v /tmp/bin:/go/bin \
 golang go get github.com/golang/example/hello/...
 /tmp/bin/hello
Hello, Go examples!

-bash:
/tmp/test/hello: cannot execute binary file

What can we do about it?

Cross-compilation

Go 1.5 comes with outstanding out-of-the-box cross-compilation abilities, so if your container operating system and/or architecture doesn’t match your system’s, it’s no problem at all!

GOOSGOARCH

For instance, assuming that you are on a 64 bits Mac:

docker run -e GOOS=darwin -e GOARCH=amd64 -v /tmp/crosstest:/go/bin \
 golang go get github.com/golang/example/hello/...
$GOPATH/bin$GOPATH/bin/$GOOS_$GOARCH./tmp/crosstest/darwin_amd64/hello.

Installing straight to the $PATH

bin
docker run -v /usr/local/bin:/go/bin \
 golang get github.com/golang/example/hello/...
/usr/usr
/tmp 

Building lean images

The Go binaries that we produced with this technique are statically linked. This means that they embed all the code that they need to run, including all dependencies. This contrasts withdynamically linked programs, which don’t contain some basic libraries (like the “libc”) and use a system-wide copy which is resolved at run time.

This means that we can drop our Go compiled program in a container, without anything else, and it should work.

Let’s try this!

The scratch image

scratch.

Let’s create a new, empty directory for our new Go lean image.

In this new directory, create the following Dockerfile:

FROM scratch
 COPY ./hello /hello
 ENTRYPOINT ["/hello"]
hellohello
hello
docker run -v $(pwd):/go/bin --rm \
 golang go get github.com/golang/example/hello/...
GOOSGOARCH

Then, we can build the image:

docker build -t hello .

And test it:

docker run hello

(This should display Hello, Go examples!.)

Last but not least, check the image’s size:

docker images hello

If we did everything right, this image should be about 2 MB. Not bad!

Building something without pushing to GitHub

Of course, if we had to push to GitHub each time we wanted to compile, we would waste a lot of time.

/gogolang$GOPATHdocker run -v $HOME/go:/go golang ....

But you can also mount local directories to specific paths, to “override” some packages (the ones that you have edited locally). Here is a complete example:

# Adapt the two following environment variables if you are not running on a Mac
 export GOOS=darwin GOARCH=amd64
 mkdir go-and-docker-is-love
 cd go-and-docker-is-love
 git clone git://github.com/golang/example
 cat example/hello/hello.go
 sed -i .bak s/olleH/eyB/ example/hello/hello.go
 docker run --rm \
 -v $(pwd)/example:/go/src/github.com/golang/example \
 -v $(pwd):/go/bin/${GOOS}_${GOARCH} \
 -e GOOS -e GOARCH \
 golang go get github.com/golang/example/hello/...
 ./hello
 # Should display "Bye, Go examples!"

 

The special case of the net package and CGo

netnet 

How do we work around that?

Re-using another distro’s libc

FROM scratchFROM debianFROM fedora

Note: you cannot use Alpine in that case, since Alpine is using the musl library instead of the GNU libc.

Bring your own libc

COPY.
ldd

Producing static binaries with netgo

netgo 
-tags netgo -installsuffix netgogo get
-tags netgo-installsuffix netgogo getgo build

The special case of SSL certificates

There is one more thing that you have to worry about if your code has to validate SSL certificates; for instance if it will connect to external APIs over HTTPS. In that case, you need to put the root certificates in your container too, because Go won’t bundle those into your binary.

Installing the SSL certificates

Three again, there are multiple options available, but the easiest one is to use a package from an existing distribution.

Dockerfile
FROM alpine:3.4
RUN apk add --no-cache ca-certificates apache2-utils

 

Check it out; the resulting image is only 6 MB!

--no-cacheapkapt-get update && apt-get install ... && rm -rf /var/cache/apt/*

As an added bonus, putting your application in a container based on the Alpine image gives you access to a ton of really useful tools: now you can drop a shell into your container and poke around while it’s running, if you need to!

Wrapping it up

We saw how Docker can help us to compile Go code in a clean, isolated environment; how to use different versions of the Go toolchain; and how to cross-compile between different operating systems and platforms.

We also saw how Go can help us to build small, lean container images for Docker, and described a number of associated subtleties linked (no pun intended) to static libraries and network dependencies.

Beyond the fact that Go is really good fit for a project that Docker, we hope that we showed you how Go and Docker can benefit from each other and work really well together!

Acknowledgements

This was initially presented during the hack day at GopherCon 2016.

I would like to thank all the people who proofread this material and gave ideas and suggestions to make it better; including but not limited to:

All mistakes and typos are my own; all the good stuff is theirs! ☺