The Go 1.11 web service Dockerfile

Build with Modules, Ship from Scratch

If you use dep, check out my previous post instead.

Goals:

  • The application executable is compiled inside a container, in order to boost reproducibility
  • The resulting image must be as small as possible
  • The application must run in a container as secure as possible: an unprivileged user in a minimal environment
  • The application must be able to make HTTPS calls

It is a multistage Dockerfile: the first throwaway stage is used for building, while the final image will only contain the compiled binary executable.

The dependencies are fetched at build time using the go.mod and go.sum files; an alternative Dockerfile for vendored dependencies is available at the bottom of this post.

  • Line 3: the ARG keyword fetches an optional command line argument. For example: docker build --build-arg GO_VERSION=1.11.2. If not provided, the default value 1.11 is used.
  • Line 10: create the unprivileged Linux group and user that will run the executable. Running an executable as root, even inside a container, is generally not advisable, specifically unnecessary, and definitely inelegant.
  • Line 17: use the Alpine package management system to install the root certificates needed for making HTTPS calls from inside the container. If you don’t plan to run an HTTPS client, you can remove this line and line 42.
  • Line 31: compile the Go application as a static binary. Because of CGO_ENABLED=0, the executable will not depend on the system C libraries and will instead embed everything that is needed to run; only statically linked binaries can run in a scratch container.
  • Line 36: FROM scratch tells Docker to start over, using a new empty base image. The first stage, FROM golang, will be discarded. The final Docker image will only contain this stage, which doesn’t even carry an operating system.
  • COPY --from=builder fetches files from the previous image, which we have named builder on line 6.
  • Line 50: tell Docker the port the application will bind to. I usually use port 8080 for exposing HTTP.
  • Line 53: thanks to the group and user files we’ve generated and imported from the first stage, there is now an unprivileged nobody user ready to be used.
  • Line 56: the arguments to ENTRYPOINT are given as a JSON array: if instead a string had been passed, Docker would have invoked sh as a tokenizer. But since there is no sh in a scratch container, it would have resulted in an error like stat /bin/sh: no such file or directory.

Alternative Dockerfile for vendored dependencies

Locking the dependencies using the go.sum integrity checks will make sure that the code downloaded for a production build will match the dependencies you’ve set up locally… provided that they are still available on the remote origin.

Since I’m not paying per MB in my repository hosting, I often vendor dependencies to ensure both integrity and availability. Go 1.11 with enabled modules will only build using vendored dependencies when passed the flag -mod=vendor.

If you are using a .dockerignore exclusion file, remember to remove vendor/ from it and run go mod vendor from your host machine, as this Dockerfile will expect all the dependencies already vendored.