Use Multi-Stage Builds to Improve Continuous Delivery Pipeline

Learn how to use Docker multi-stage builds

ta-ching chen

3 minute read

 Table of Contents

Introduction 

A basic rule for building a container image is “the smaller is better”.

But it’s not an easy job for the compiled language (e.g. go, java) due to large build environment and dependencies. In the post “Building Minimal Docker Image for Go Applications”, we need to produce a go binary first and put it into scratch image. And it’s quite complicated and heavy for developers to create such continuous delivery pipeline.

To solve this problem, Docker 17.05+ releases a new feature called Multi-stage builds. With this feature we can combine multiple dockerfiles into one and let base image to copy artifacts and outputs from the intermediate image. In this way, we can keep pipeline easy to read and maintain.


Hands-On 

Dockerfile example of multi-stage builds

FROM alpine AS base
RUN apk add --no-cache curl wget

FROM golang:1.9.2 AS go-builder
WORKDIR /go
COPY *.go /go/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .

FROM base
COPY --from=go-builder /go/main /main
CMD ["/main"]

Here is a diagram to explain what happened during the builds.

mulit-stage-builds

Stages 

Stage 0 (Stage base) 

FROM alpine AS base
RUN apk add --no-cache curl wget
  • Name stage 0 as base by adding as <NAME>.
  • Use alpine as parent image to create an intermediate image, and install required packages (curl, wget).

Stage 1 (Stage go-builder) 

FROM golang:1.9.2 AS go-builder
WORKDIR /go
COPY *.go /go/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .
  • Name stage 1 as go-builder.
  • Compile packages and dependencies.

Stage 2 

FROM base
COPY --from=go-builder /go/main /main
CMD ["/main"]
  • Use intermediate image as parent image in stage 0 (stage build).
  • Use COPY command copy artifacts generated in stage 1 (stage go-builder).
    • COPY --from=1
    • COPY --from=<intermediate_image_name>

Tips 

  1. FROM <base_image> as <stage_name>

    • Always name stage for readability.
    • No need to modify existing COPY commands if more stages add in future.
  2. COPY --from=<stage_name>

    • Use stage name instead of number of stage for readability and maintenance.

Build image

$ docker build -t multi_stage_minimal_go_docker_img .

You should see log outputs like following

Sending build context to Docker daemon  3.072kB
Step 1/9 : FROM alpine AS base
 ---> 3fd9065eaf02
Step 2/9 : RUN apk add --no-cache curl wget
 ---> Running in c1c75ffbfef4
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ca-certificates (20171114-r0)
(2/5) Installing libssh2 (1.8.0-r2)
(3/5) Installing libcurl (7.59.0-r0)
(4/5) Installing curl (7.59.0-r0)
(5/5) Installing wget (1.19.2-r1)
Executing busybox-1.27.2-r7.trigger
Executing ca-certificates-20171114-r0.trigger
OK: 6 MiB in 16 packages
Removing intermediate container c1c75ffbfef4
 ---> 6061a54c31c9
Step 3/9 : FROM golang:1.9.2 AS go-builder
 ---> 138bd936fa29
Step 4/9 : WORKDIR /go
Removing intermediate container e7a1f8df0451
 ---> 8f440d314727
Step 5/9 : COPY *.go /go/
 ---> 95f1803ce6b3
Step 6/9 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .
 ---> Running in 72cba3033d5c
Removing intermediate container 72cba3033d5c
 ---> 9a8875b6eec1
Step 7/9 : FROM base
 ---> 6061a54c31c9
Step 8/9 : COPY --from=go-builder /go/main /main
 ---> 99e564fa87b8
Step 9/9 : CMD ["/main"]
 ---> Running in 7751604e5d0c
Removing intermediate container 7751604e5d0c
 ---> 50aa7d144269
Successfully built 50aa7d144269
Successfully tagged multi_stage_minimal_go_docker_img:latest

That’s it! Now, we can produce a small docker image without too much effort!

Reference 

comments powered by Disqus