Build with Modules, Ship from Scratch
If you use dep, check out my previous post instead.
- 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.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
- 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
- Line 36:
FROM scratchtells 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=builderfetches files from the previous image, which we have named
builderon 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
nobodyuser ready to be used.
- Line 56: the arguments to
ENTRYPOINTare given as a JSON array: if instead a string had been passed, Docker would have invoked
shas a tokenizer. But since there is no
shin 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
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.
This post originally appeared here.