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.
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
There are two aspects to this — keeping the build time low and keeping the resulting image small.
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
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.
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.