DEV Community

Ivan Dlugos
Ivan Dlugos

Posted on • Originally published at Medium on

Go — build a minimal docker image in just three steps

Docker & Go image by github.com/ashleymcnamara/gophers

Go — build a minimal docker image in just three steps

When you build your Go application for docker, you usually start from some image like golang:1.13. However, it’s a waste of resources to actually use that image for runtime. Let’s take a look at how you can build a Go application as an absolute minimal docker image.


1. Choose a Go version

While it might be tempting to use golang:latest or just golang there are many reasons why this is not such a good idea but the chief one among them is build repeatability. Whether it’s about using the same version for production deployment that you’ve developed and tested on, or if you find yourself in a need to patch an old version of your application, it’s a good idea to keep the Go version pinned to a specific release and only update it when you know it’s going to work with a newer one.

Therefore, always use full specification, including the patch version number and ideally even the base OS that image comes from, e.g. 1.13.0-alpine3.10

2. Keep it minimal

There are two aspects to this — keeping the build time low and keeping the resulting image small.

Fast builds

Docker caches intermediate layers for you so if you structure your Dockerfile right, you can reduce the time it takes for each subsequent rebuild (after a change). The rule of a thumb is to order the commands based on how frequently their source (e.g. source of a COPY) is going to change.

Also, consider using a .dockerignore file which helps keep the build context small — basically, when you run docker build, docker needs to feed everything in the current directory to the build daemon (that Sending build context to Docker daemon message you see at the beginning of a docker build). In short, if your repo contains a lot of data not necessary for building your app (such as tests, markdown for docs generator, etc), .dockerignore will help to speed the build up. At the very least, you can start with the following contents. Dockerfile is there so that if you COPY . . (which you shouldn’t, BTW) doesn’t have to execute and invalidate everything bellow when you change just that Dockerfile.

.git
Dockerfile
testdata

Small images

Very simple — use scratch. Nothing else comes close (because it can’t). Scratch is a special “base” image in that it’s not really an actual image but a completely empty system. Note: in an older version of docker, an explicit scratch image was actually used as a layer, this is no longer the case as of docker 1.5.

How this works is that you use a two-step build inside a single Dockerfile, where you actually build your app on one image, called builder (as an example, it can be actually any name you fancy), then copy the resulting binaries (and all other required files) to a final image based on scratch.

3. Putting it all together

Let’s see how a complete Dockerfile looks like, shall we?

Please leave a comment if you find this useful and/or you would like to share a few tips or tricks of your own.

Latest comments (2)

Collapse
 
uromahn profile image
Ulrich Romahn

Just one word of caution for those wanting to use the "scratch" container in production: if your binary you are packaging with the scratch container has some dependencies on a specific version of some very specific libraries, e.g. glibc, your app may not run when the container is being started on a host with a different OS than the one you used to package the container.
For example, you package your container on CentOS 7 but then your container gets to run on an older CentOS 6. In this case, binaries depending on glibc may fail to run since the glibc that comes with CentOS 7 is not backwards compatible with the older glibc coming with CentOS 6.

So, while it is really desirable to keep the container size as minimal as possible, one of the benefits of containers is to shield the app from the specifics of the underlying host OS providing it the same runtime no matter where the container will be run. And, the only way to really do that is to package a minimal OS with the container for your application.

Just my 2 cents.

Collapse
 
ivan profile image
Ivan Dlugos • Edited

Thanks for the update. I am aware of issues with mismatching glibc versions but was under an impression that an application wouldn't even start with a library missing from the image. That's why the Dockerfile contains library collection script. Here's a dive printout of a sample image contents for a dynamically linked application.

[Aggregated Layer Contents]────────────────────────────────
Permission     UID:GID       Size  Filetree
drwxr-xr-x     65534:0        0 B  ├── data 
drwxr-xr-x         0:0     4.8 MB  ├── lib64                   
-rwxr-xr-x         0:0     163 kB  │   ├── ld-linux-x86-64.so.2
-rwxr-xr-x         0:0     2.2 MB  │   ├── libc.so.6 
-rwxr-xr-x         0:0      19 kB  │   ├── libdl.so.2   
-rwxr-xr-x         0:0      89 kB  │   ├── libgcc_s.so.1
-rwxr-xr-x         0:0     1.1 MB  │   ├── libm.so.6      
-rwxr-xr-x         0:0     142 kB  │   ├── libpthread.so.0
-rwxr-xr-x         0:0     992 kB  │   ├── libstdc++.so.6
-rwxr-xr-x         0:0      90 kB  │   └── libz.so.1
-rwxr-xr-x         0:0     7.7 MB  └── service

However, I'll certainly have a look at how this behaves on an ancient system. Your mention of CentOS 6 is a good example of one still fairly used (unfortunately).