Using Multi-stage Builds in Dockerfile

Using Multi-stage Builds in Dockerfile

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

Share Tweet Send
0 Comments
Loading...