Wednesday, December 4, 2019

Use cache in a docker multi stage build


Docker multi stage build (see docker official documentation) provides ability to both compile and create final image in a single docker file.
The logic is as follows:

Stage 1:
  • Use the related compiler docker image
  • Compile the sources
  • Create the final executable binary file

Stage 2:
  • Use the production related docker image (e.g. ubuntu/alpine)
  • Copy the executable binary file from stage 1
  • Run any additional required final image preparation steps


For example, a two-stages build for a GO image is:
# Stage1: compile
FROM golang:1.12 AS build-env
RUN apt-get update && apt-get install -y git

WORKDIR /src
ENV GOPATH=/go
ENV GOBIN=/go/bin
COPY ["./src/go.mod", "./src/go.sum", "/src/"]
RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go mod download
ADD ./src /src
RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -a -installsuffix cgo -o final-binary

# Stage2: package
FROM ubuntu:18.04
COPY --from=build-env /src/my-final-binary /final-binary
WORKDIR /
ENTRYPOINT ["/final-binary"]

However, when running the docker build, I've noticed that the first stage keeps running every time for a long time, even that I did not change anything in the source.
It is due to the fact that docker multi stage build does not keep cache for the intermediate steps, as described here.

The solution I've used is to split the docker build into two builds.
The first build docker image is not used, but exists only for caching purpose.
So the actual final build contains the following files:

  • Dockerfile_stage1
  • Dockerfile_stage2
  • build.sh
Let's review the files. First, the Dockerfile_stage1, which includes the first section of the original Dockerfile.

# Stage1: compile
FROM golang:1.12 AS build-env
RUN apt-get update && apt-get install -y git

WORKDIR /src
ENV GOPATH=/go
ENV GOBIN=/go/bin
COPY ["./src/go.mod", "./src/go.sum", "/src/"]
RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go mod download
ADD ./src /src
RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -a -installsuffix cgo -o final-binary

Next, the Dockerfile_stage2, which includes the second section of the original Dockerfile.

# Stage2: package
FROM ubuntu:18.04
COPY --from=build-env /src/my-final-binary /final-binary
WORKDIR /
ENTRYPOINT ["/final-binary"]

And last, the build.sh that does all the magic.

#!/usr/bin/env bash
docker build -d Dockerfile_stage1 -t "local/myimage-stage1:latest"

cat Dockerfile_stage1 > Dockerfile_full
cat Dockerfile_stage2 >> Dockerfile_full

docker build -d Dockerfile_full -t "local/myimage:latest"

rm -f Dockerfile_full


Summary

The docker not caching multi stage builds issue can be simply bypassed easily using the method described above. The method also avoids using complicated steps as described in other solutions, and it prevents from using duplicate code among files.



1 comment:

  1. Thanks for sharing Alon, clever trick!

    How would you integrate this to use in a compose build?

    ReplyDelete