how to build the smallest docker image as fast as you can

how to build the smallest docker image as fast as you can

I’ll use an example to introduce how to build the smallest docker image to your best, a light image will accelerate image rollout and the fast build process will speed up your development cycle.

A Golang sample

It’s quite common that we use golang to implement microservice, for example tens of golang service docker images are deployed to the Kubernetes.

Consider we need to make our first golang service image, it runs the following code as an HTTP server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"net/http"
)

func main() {
http.HandleFunc("/", test)
http.ListenAndServe(":8080", nil)
}

func test(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Service", "Test")
w.WriteHeader(200)
}

Docker it!

The intuitive solution is to run go run main.go in the docker, so let’s use golang image as a base image.

1
2
3
4
5
FROM golang:1.14.0-alpine

WORKDIR /go/src/github.com/songrgg/testservice/
COPY main.go .
CMD [ "go", "run", "main.go" ]

Run docker build . and it shows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM golang:1.14.0-alpine
---> 51e47ee4db58
Step 2/4 : WORKDIR /go/src/github.com/songrgg/testservice/
---> Using cache
---> 8dc325ca7ca6
Step 3/4 : COPY main.go .
---> Using cache
---> c46c5f5bfda8
Step 4/4 : CMD [ "go", "run", "main.go" ]
---> Using cache
---> acc5a6d462f5
Successfully built acc5a6d462f5

$ docker image inspect acc5a6d462f5 --format='{{.Size}}'
369193951

It’s 369193951 bytes near 370MB.

Reduce the unnecessary files

Golang is a compilation language which can be packed into a binary, we can reduce the size by only putting the binary into the image.

We know that the latest docker supports multi-stage builds which can eliminate the intermediate layers effectively, the revised dockerfile looks like this:

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.14.0-alpine

WORKDIR /go/src/github.com/songrgg/testservice/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/songrgg/testservice/app .
CMD ["./app"]

Let’s see the layers it generates.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ docker build .
Sending build context to Docker daemon 3.072kB
Step 1/9 : FROM golang:1.14.0-alpine
---> 51e47ee4db58
Step 2/9 : WORKDIR /go/src/github.com/songrgg/testservice/
---> Using cache
---> 8dc325ca7ca6
Step 3/9 : COPY main.go .
---> Using cache
---> c46c5f5bfda8
Step 4/9 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
---> Running in be3bdce1ec48
Removing intermediate container be3bdce1ec48
---> 9c3470f9e73d
Step 5/9 : FROM alpine:latest
---> 053cde6e8953
Step 6/9 : RUN apk --no-cache add ca-certificates
---> Running in 6acd18e2d2ba
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/community/x86_64/APKINDEX.tar.gz
(1/1) Installing ca-certificates (20161130-r3)
Executing busybox-1.26.2-r7.trigger
Executing ca-certificates-20161130-r3.trigger
OK: 5 MiB in 12 packages
Removing intermediate container 6acd18e2d2ba
---> 7b4ee3222013
Step 7/9 : WORKDIR /root/
---> Running in 336a8a2115a6
Removing intermediate container 336a8a2115a6
---> 77fa1196ab2f
Step 8/9 : COPY --from=0 /go/src/github.com/songrgg/testservice/app .
---> c6bc47f614af
Step 9/9 : CMD ["./app"]
---> Running in f77053027c4b
Removing intermediate container f77053027c4b
---> d327f978f1d8
Successfully built d327f978f1d8

$ docker inspect d327f978f1d8 --format='{{.Size}}'
11924648

We could notice there are several intermediate containers removed, they’re layers from the first stage and prepare the executable for the second stage.

Most importantly, the docker image size is below 12MB.

The most important factor is we changed the FROM image to alpine which is only 4.2MB, so the extra size is almost the size of the golang process.

From scratch?

Yeah, the base image could be smaller, FROM scratch is a base image that will make the next command to be the first layer in your image.

I changed the FROM alpine:latest to FROM scratch, the image size is 7MB now, but I would suggest using alpine because it’ll be hard for you in scratch if you want to debug within the container. So you’ll need a balance between the image size and functionality :)

Some pitfalls you may face

Unnecessary large build context

Docker build will send the build context to docker daemon at first, the context is default to the current directory, so please be sure the files in the current directory is necessary or is small enough. If the file size is big, it will affect docker build speed terribly.

Wrong command order

Just remember to put the stable layers before the changeable layers, because docker will cache the layers if they are not changed, it’s calculated by the hash value of their content.

1
2
3
4
FROM ubuntu
COPY changeable.txt .
RUN apt-get update && apt-get install curl
RUN ...

In the above example, every time the changeable.txt is changed, it will rerun every commands after it and waste time doing the things it could prevent. Just turn to the following form.

1
2
3
4
FROM ubuntu
RUN apt-get update && apt-get install curl
RUN ...
COPY changeable.txt .

Reference

  1. Best practises for writing Dockerfile

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×