Using Multi-stage builds is a good solution I found in order to write clear step-by-step Dockerfiles and at the same time get a simple, secure and lightweight image at the end.
In older Docker version this feature was not available, and I was struggling with my Dockerfile to get as fewer layers as possible to reduce the image size at the same time I was trying to keep getting a reasonable build time (or re-build time to prevent a heavy layer to be rebuilt again when only small changes were applied to the Dockerfile) Also, some projects were using multiple Dockerfiles to separate a build process from a release process, or building images with slightly differences where most of the code was a copy-paste.
Multi-stage builds solves all these problems, let’s go through a quick guide on how to convert bad practices in good practices using multi-stage Dockerfiles.
FROM golang:1.13
WORKDIR /app
COPY . .
# pre-build processing
RUN scripts/pre-build.sh
# build golang proj
RUN go mod download
RUN go build -o bin/my-app .
CMD ./my-app
Some problems we found in the dockerfile above:
- Several layers for the image for every RUN execution, COPY command also generates a new layer, so we have at least 4 layers
- Source code included in the image
- pre-build.sh generates rubbish or can be adding some private information to the image
- Image originated from golang:1.13 meaning that all the Golang tools are available by default in the image we’ve generated
How a multi-stage build solves all these problems:
FROM golang:1.13 AS builder
WORKDIR /app
COPY . .
# pre-build processing
RUN scripts/pre-build.sh
# build golang proj
RUN go mod download
RUN go build -o bin/my-app .
FROM debian:stable AS release
WORKDIR /app
COPY --from=builder /app/bin/my-app .
CMD ./my-app
In the file we start with a builder image that will run exactly the same process we had before, and it will generate multiple layers, but this will not be our final image for distribution, we start a new release stage using a debian:stable image as our base image, we take specific files from the previous stage (in this case we take the generated executable file we’ve obtained from go build output), finally we specify this is the command to be executed for the container.
As you can see all the source code, as well dependencies downloaded from Go for the build, and even all the Golang related tools from its docker image, are not part of the final build, and we can distribute a much cleaner, secure and small image with the binary file only.
Cover image credits:
Technology vector created by macrovector - www.freepik.com