Docker + Golang = <3

December 8, 2025 · 1727 words · 9 min

This is a short collection of tips and tricks showing how Docker can be useful when working with Go

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). 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. If any of this is relevant to you, then let’s call Docker to the rescue! When you have installed Go, you can do  to download, build, and install a library. (The  flag is just here for verbosity, you can remove it if you prefer your toolchain to be swift and silent!) You can also do  (yes, that’s three dots) to download, build, and install all the things in that repo (including libraries and binaries). We can do that in a container! Try this: 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? One solution is to  the container that we just built, i.e. “freeze” it into a new image: Note:  outputs the ID (and only the ID!) of the last container that was executed. If you are the only uesr on your machine, and if you haven’t created another container since the previous command, that container should be the one in which we just built the “hello world” example. Now, we can run our program in a container based on the image that we just built: The output should be . When creating the image with , you can use the  flag to specify arbitrary  commands. For instance, you could use a  or  command so that  automatically executes hello. What if we don’t want to create an extra image just to run this Go program? We got you covered: Wait a minute, what are all those bells and whistles? When you use the image, Docker expands that to which (as you might guess) will map to the latest version available on the Docker Hub. If you want to use a specific version of Go, that’s very easy: specify that version as a  after the image name. For instance, to use Go 1.5, change the example above to replace  with : You can see all the versions (and variants) available on the  on the Docker Hub. 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!) The easiest way to get the binary out of the container is to map the  directory to a local directory. In the  container,  is So we can do the following: If you are on Linux, you should see the  message. But if you are, for instance, on a Mac, you will probably see: -bash:

/tmp/test/hello: cannot execute binary file What can we do about it? Go 1.5 comes with , so if your container operating system and/or architecture doesn’t match your system’s, it’s no problem at all! To enable cross-compilation, you need to set  and/or . For instance, assuming that you are on a 64 bits Mac: The output of cross-compilation is not directly in , but in In other words, to run the program, you have to execute If you are on Linux, you can even install directly to your system  directories: However, on a Mac, trying to use  as a volume will not mount your Mac’s filesystem to the container. It will mount the  directory of the Moby VM (the small Linux VM hidden behind the Docker whale icon in your toolbar). You can, however, use or something in your home directory, and then copy it from there. The Go binaries that we produced with this technique are . This means that they embed all the code that they need to run, including all dependencies. This contrasts with  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, , and it should work. Let’s try this! There is a special image in the Docker ecosystem: This is an empty image. It doesn’t need to be created or downloaded, since by definition, it is empty. Let’s create a new, empty directory for our new Go lean image. In this new directory, create the following Dockerfile: This means: * start  (an empty image), * add the  file to the root of the image, * define this  program to be the default thing to execute when starting this container. Then, produce our  binary as follows: Note: we don’t need to set  and  here, because precisely, we want a binary that will run , not on our host system. So leave those variables alone! Then, we can build the image: And test it: (This should display Hello, Go examples!.) Last but not least, check the image’s size: If we did everything right, this image should be about 2 MB. Not bad! Of course, if we had to push to GitHub each time we wanted to compile, we would waste a lot of time. When you want to work on a piece of code and build it within a container, you can mount a local directory to  in the  container, so that the  is persisted across invocations: 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:   Before diving into real-world Go code, we have to confess something: we lied a little bit about the static binaries. If you are using CGo, or if you are using the  package, the Go linker will generate a dynamic binary. In the case of the package (which a  of useful Go programs out there are using indeed!), the main culprit is the DNS resolver. Most systems out there have a fancy, modular name resolution system (like the ) which relies on plugins which are, technically, dynamic libraries. By default, Go will try to use that; and to do so, it will produce dynamic libraries. How do we work around that? One solution is to use a base image that  the essential libraries needed by those Go programs to function. Almost any “regular” Linux distro based on the GNU libc will do the trick. So instead of , you would use  or , for instance. The resulting image will be much bigger now; but at least, the bigger bits will be shared with other images on your system. Note: you  use Alpine in that case, since Alpine is using the musl library instead of the GNU libc. Another solution is to surgically extract the files needed, and place them in your container with The resulting container will be small. However, this extraction process leaves the author with the uneasy impression of a really dirty job, and they would rather not go into more details. If you want to see for yourself, look around  and the Name Service Switch plugins mentioned earlier. We can also instruct Go to  use the system’s libc, and substitute Go’s library, which comes with a native DNS resolver. To use it, just add  to the  options. 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. Three again, there are multiple options available, but the easiest one is to use a package from an existing distribution. Alpine is a good candidate here because it’s so tiny. The following  will give you a base image that is small, but has an up-to-date bundle of root certificates:   Check it out; the resulting image is only 6 MB! Note: the  option tells  (the Alpine package manager) to get the list of available packages from Alpine’s distribution mirrors, without saving it to disk. You might have seen Dockerfiles doing something like ; this achieves something equivalent (i.e. not leave package caches in the final image) with a single flag.  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! 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! 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! ☺