Let’s modify the Dockerfile according to the multi-stage build process and see what we get.
The respective files for this example are present in the ch4/go-hello-world/multi-stage directory within this book’s GitHub repository.
The following is the Dockerfile:
FROM golang:1.20.5 AS build
WORKDIR /tmp
COPY app.go .
RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o app . && chmod +x ./app
FROM alpine:3.18.0
WORKDIR /tmp
COPY –from=build /tmp/app .
CMD [“./app”]
The Dockerfile contains two FROM directives: FROM golang:1.20.5 AS build and FROM alpine:3.18.0. The first FROM directive also includes an AS directive that declares the stage and names it build. Anything we do after this FROM directive can be accessed using the build term until we encounter another FROM directive, which would form the second stage. Since the second stage is the one we want to run our image from, we are not using an AS directive.
In the first stage, we build our Golang code to generate the binary using the golang base image.
In the second stage, we use the Alpine base image and copy the /tmp/app file from the build stage into our current stage. This is the only file we need to run in the container. The rest were only required to build and bloat our container during runtime.
Let’s build the image and see what we get:
$ docker build -t <your_dockerhub_user>/go-hello-world:multi_stage [+] Building 12.9s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 259B 0.0s => [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18.0 2.0s => [internal] load metadata for docker.io/library/golang:1.20.5 1.3s
=> [build 1/4] FROM docker.io/library/golang:1.20.5@sha256:4b1fc02d… 0.0s => [stage-1 1/3] FROM docker.io/library/alpine:3.18.0@sha256:02bb6f42… 0.1s => => resolve docker.io/library/alpine:3.18.0@sha256:02bb6f42… 0.0s => => sha256:c0669ef3… 528B / 528B 0.0s
=> => sha256:5e2b554c… 1.47kB / 1.47kB 0.0s
=> => sha256:02bb6f42… 1.64kB / 1.64kB 0.0s
=> CACHED [build 2/4] WORKDIR /tmp 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 108B 0.0s
=> [build 3/4] COPY app.go . 0.0s
=> [build 4/4] RUN GO111MODULE=off GOOS=linux go build -a -installsuffix cgo -o app . && chmod +x ./app 10.3s
=> [stage-1 2/3] WORKDIR /tmp 0.1s
=> [stage-1 3/3] COPY –from=build /tmp/app . 0.3s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:e4b793b3… 0.0s
=> => naming to docker.io/<your_dockerhub_user>/go-hello-world:multi_stage
Now, let’s run the container:
$ docker run <your_dockerhub_user>/go-hello-world:multi_stage .
Hello, World!
We get the same output, but this time with a minimal footprint. Let’s look at the image to confirm this:
$ docker images
REPOSITORY
TAG
IMAGE ID
CREATED
SIZE
<your_dockerhub_user>
/go-hello-world
multi_stage
e4b793b39a8e
5 minutes ago
9.17MB
This one occupies just 9.17 MB instead of the huge 803 MB. This is a massive improvement! We have reduced the image size by almost 100 times.
That is how we increase efficiency within our container image. Building efficient images is the key to running production-ready containers, and most professional images you find on Docker Hub use multi-stage builds to create efficient images.
Tip
Use multi-stage builds where possible to include minimal content within your image. Consider using an Alpine base image if possible.
In the next section, we will look at managing images within Docker, some best practices, and some of the most frequently used commands.