Multi-stage builds
Multi-stage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.
Without Multi-stage builds
Before Docker 17.05, there were typically two ways to build Docker images:
Put everything in one Dockerfile
One way was to include all the build steps in a single Dockerfile
, including the compilation, testing, and packaging of the project and its dependencies. This could lead to some issues:
-
Multiple image layers, larger image size, and longer deployment times
-
Risk of source code leakage
For example, write an app.go
file that prints Hello World!
package main
import "fmt"
func main(){
fmt.Printf("Hello World!");
}
Write a Dockerfile.one
file
FROM golang:alpine
RUN apk --no-cache add git ca-certificates
WORKDIR /go/src/github.com/go/helloworld/
COPY app.go .
RUN go get -d -v github.com/go-sql-driver/mysql \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
&& cp /go/src/github.com/go/helloworld/app /root
WORKDIR /root/
CMD ["./app"]
Build the image
$ docker build -t go/helloworld:1 -f Dockerfile.one .
Split into multiple Dockerfiles
The other way was to first compile, test, and package the project and its dependencies in one Dockerfile
, and then copy the artifacts to the runtime environment in another Dockerfile
. This approach required writing two Dockerfiles
and some build scripts to automate the integration of the two stages, which made the deployment process more complex, although it avoided the risks of the first approach.
For example, write a Dockerfile.build
file
FROM golang:alpine
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld
COPY app.go .
RUN go get -d -v github.com/go-sql-driver/mysql \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
Write a Dockerfile.copy
file
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]
Create a build.sh
script
#!/bin/sh
echo Building go/helloworld:build
docker build -t go/helloworld:build . -f Dockerfile.build
docker create --name extract go/helloworld:build
docker cp extract:/go/src/github.com/go/helloworld/app ./app
docker rm -f extract
echo Building go/helloworld:2
docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
rm ./app
Now run the script to build the image
$ chmod +x build.sh
$ ./build.sh
Compare the image sizes generated by the two approaches
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
go/helloworld 2 f7cf3465432c 22 seconds ago 6.47MB
go/helloworld 1 f55d3e16affc 2 minutes ago 295MB
Using Multi-stage builds
To solve the above problems, Docker v17.05 introduced support for multi-stage builds. Using multi-stage builds, we can easily solve the problems mentioned earlier, and only need to write a single Dockerfile
:
For example, write a Dockerfile
file
FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]
Build the image
$ docker build -t go/helloworld:3 .
Compare the sizes of the three images
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
go/helloworld 3 d6911ed9c846 7 seconds ago 6.47MB
go/helloworld 2 f7cf3465432c 22 seconds ago 6.47MB
go/helloworld 1 f55d3e16affc 2 minutes ago 295MB
It is clear that the image built using multi-stage builds is smaller in size, while also perfectly solving the problems mentioned earlier.
Use a previous stage as a new stage
When using multi-stage builds, you aren't limited to copying from stages you created earlier in your Dockerfile. You can use the COPY --from instruction to copy from a separate image, either using the local image name, a tag available locally or on a Docker registry, or a tag ID. The Docker client pulls the image if necessary and copies the artifact from there. The syntax is:
FROM golang:alpine as builder
For example, if we only want to build the image for the builder
stage, add the --target=builder
parameter:
$ docker build --target builder -t username/imagename:tag .
Use an external image as a stage
You can pick up where a previous stage left off by referring to it when using the FROM directive.
In the above example, we used COPY --from=0 /go/src/github.com/go/helloworld/app .
to copy files from the previous stage's image. We can also copy files from any other image.
$ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf