Reduce the size of Golang images with multi-stage build
How can we reduce the size of Golang images by introducing Dockerfiles with a multi-stage build process
Let’s start from a generic Dockerfile that takes care of the basic staff like dependencies, building the binaries, expose the necessary ports etc. for a very basic REST API in Go.
FROM golang:1.16-alpineENV GO111MODULE=onWORKDIR /appCOPY go.mod .
COPY go.sum .RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go buildENV HTTP_PORT=8080
EXPOSE 8080ENTRYPOINT ["/app/reduce-docker-size"]
That will seamlessly build the binaries of your project and create the docker image.
Is it that good enough? I would say no, because the size of the generated image is more than 300MB (322MB to be precise) as it includes all the Golang tooling, which in the end of the day is unecessary to us as we instruct the compiler to disable cgo (CGO_ENABLED=0) and statically link any C bindings that will provide us a self-contained executable (whose size is just 6.05MB!), with no external framework or runtime dependencies.
CGO_ENABLED=0 is vital, if we don’t build self-contained executables the multi-stage build process won’t work.
What can we do better though, is employing a so called multi-stage build. A multi-stage build allows multiple distinct builds, that can derive from completely different base images, to selectively pass artifacts down the line from one stage to the next stripping the final image from all the unecessary luggage. For instance we can call the previous stage as BUILD and then introduce a second stage, let’s call it BINARIES, that uses alpine:latest as base image and copy there the binaries of our built application from the BUILD stage.
# BUILDFROM golang:1.16-alpine as BUILDENV GO111MODULE=onWORKDIR /appCOPY go.mod .
COPY go.sum .RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go buildENV HTTP_PORT=8080
EXPOSE 8080# BINARIESFROM alpine:latestCOPY --from=BUILD /app/reduce-docker-size /app/reduce-docker-sizeENTRYPOINT ["/app/reduce-docker-size"]
Tights with golang toolkit were cut, as they are not needed anymore. Image size now dropped at 11.7MB.
Is it that good enough? I would say yes, but let’s push the limits a bit more just for the sake of the experiment. We stay on the path of multi-stage builds but this time instead of using alpine:latest for our second stage we are going to resort to a very special image called scratch, that is an empty image containing literally nothing.
# BUILDFROM golang:1.16-alpine as BUILDENV GO111MODULE=onWORKDIR /appCOPY go.mod .
COPY go.sum .RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go buildENV HTTP_PORT=8080
EXPOSE 8080# MINIATUREFROM scratchCOPY --from=BUILD /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=BUILD /app/reduce-docker-size /app/reduce-docker-sizeENTRYPOINT ["/app/reduce-docker-size"]
The newly created image now dropped at 6.34MB!
Because scratch image, as we foretold, is literally empty, there are no root SSL certificates to be found. The following directive copies the certificates in the final image and should not to be omitted by any means:
COPY — from=BUILD /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
Is using scratch as base image for the final stage worthy? I would say yes and no. If you exclude some corner-cases — that those 5,36MB difference between alpine:latest and scratch derived end-images would potentially make an enormous impact — in the rest of the cases you will end up, in production, with a container that has absolutely no tooling within and I would totally not recommend it. Those corner-cases are rare, so think twice before putting yourself in trouble for just 5,36MB (which in practice is the size of the alpine:latest, which is by itself something pretty remarkable if you think about it).
If you want additionally to learn how to automate your build process with GitLab CI Pipelines then have a look in this guide of mine:
Have fun with Golang, it is truly a joy developing with it, and you will definetely not regret the time you’ll invest in the long run.