Using multistage and UPX to build smaller Docker images

Keywords: Docker github Linux shell

Catalog

Single-stage construction

Multistage Construction Mirror

Using UPX to build smaller Docker images

They will use the following mirrors to build new mirrors:

$ docker images
REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
golang                                     1.10.3              d0e7a411e3da        6 weeks ago         794MB
alpine                                     3.8                 11cd0b38bc3c        8 weeks ago         4.41MB

Single-stage construction

Dockerfile

FROM golang:1.10.3
WORKDIR /go/src/test
RUN go get github.com/gin-gonic/gin
COPY src src
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go
CMD ["./main"]

Build

$ docker build -t zev/test:1.0.0 .

Sending build context to Docker daemon  17.41kB
Step 1/6 : FROM golang:1.10.3
 ---> d0e7a411e3da
Step 2/6 : WORKDIR /go/src/test
 ---> Running in 94d1ede51e17
Removing intermediate container 94d1ede51e17
 ---> 2b643ce8b3cf
Step 3/6 : RUN go get github.com/gin-gonic/gin
 ---> Running in de5e9adb7c10
Removing intermediate container de5e9adb7c10
 ---> ff970f45de1e
Step 4/6 : COPY src src
 ---> 6b79fef06e45
Step 5/6 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go
 ---> Running in 6d4ef8c0b580
Removing intermediate container 6d4ef8c0b580
 ---> 59678a3ab4d8
Step 6/6 : CMD ["./main"]
 ---> Running in a5cea54f2ccb
Removing intermediate container a5cea54f2ccb
 ---> a253cfcddd6a
Successfully built a253cfcddd6a
Successfully tagged zev/test:1.0.0

RUN

$ docker run -it -p 8080:8080 zev/test:1.0.0  
        
[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080

Images

$ docker images

REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
zev/test                                   1.0.0               a253cfcddd6a        4 minutes ago       857MB

Image's size is 857MB, which includes the whole golang environment. Such a large file is absolutely a disaster in transmission. Next, we use multi-stage to build a relatively small image.

Multistage Construction Mirror

Each layer of the Docker image records only file changes. When the container starts up, Docker calculates each layer of the image and finally generates a file system, which is called joint mounting.

Docker's layers are relevant. In the process of joint mounting, the system needs to know on what basis to add new files. This requires a Docker image to have only one starting layer and only one root. Therefore, only one FROM instruction is allowed in the Docker Docker Docker Dockerfile. Because multiple FROM instructions can cause multiple roots, it is impossible to implement.

Docker 17.05 later allows Dockerfile to support multiple FROM instructions. Multiple FROM instructions are not intended to generate multiple layers. The final image generated is still based on the last FROM, and the previous FROM will be discarded. Each FROM instruction is a construction phase, and multiple FROM instructions are multi-stage construction. Although the final image generated can only be the result of the last phase, the files in the pre-stage can be copied to In the later stage, this is the greatest significance of multi-stage construction.

The biggest usage scenario for multi-stage construction is to separate the compilation environment from the runtime environment:

DockerFile

FROM golang:1.10.3 as builder
WORKDIR /go/src/test
RUN go get github.com/gin-gonic/gin
COPY src src
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go

FROM alpine:3.8
WORKDIR /root
COPY --from=builder /go/src/test/main .
CMD ["./main"]

Build

$ docker build -t zev/test:1.0.1 .                                                                                                            

Sending build context to Docker daemon  17.41kB
Step 1/9 : FROM golang:1.10.3 as builder
 ---> d0e7a411e3da
Step 2/9 : WORKDIR /go/src/test
 ---> Using cache
 ---> 2b643ce8b3cf
Step 3/9 : RUN go get github.com/gin-gonic/gin
 ---> Using cache
 ---> ff970f45de1e
Step 4/9 : COPY src src
 ---> Using cache
 ---> 6b79fef06e45
Step 5/9 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go
 ---> Using cache
 ---> 59678a3ab4d8
Step 6/9 : FROM alpine:3.8
 ---> 11cd0b38bc3c
Step 7/9 : WORKDIR /root
 ---> Running in 1640c71479d6
Removing intermediate container 1640c71479d6
 ---> ec68dc839562
Step 8/9 : COPY --from=builder /go/src/test/main .
 ---> 5bb444c91aff
Step 9/9 : CMD ["./main"]
 ---> Running in a80305feba6e
Removing intermediate container a80305feba6e
 ---> 5923597f59c2
Successfully built 5923597f59c2
Successfully tagged zev/test:1.0.1

RUN

$ docker run -it -p 8080:8080 zev/test:1.0.1  

[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080

Images

$ docker images

REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
zev/test                                   1.0.1               5923597f59c2        2 minutes ago       19.8MB

Multistage builds have reduced images by 40 times, and 19.8M size works well in both test and production environments, but is that over?
Of course not. Our goal is to make the image smaller. Let's see what we do next.

Using UPX to build smaller Docker images

Dockerfile

FROM golang:1.10.3 as builder
RUN apt-get update && apt-get install -y xz-utils && rm -rf /var/lib/apt/lists/*
ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz /usr/local
RUN xz -d -c /usr/local/upx-3.95-amd64_linux.tar.xz | tar -xOf - upx-3.95-amd64_linux/upx > /bin/upx && chmod a+x /bin/upx
WORKDIR /go/src/test
RUN go get github.com/gin-gonic/gin
COPY src src
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go
RUN strip --strip-unneeded main
RUN upx main

FROM alpine:3.8
WORKDIR /root
COPY --from=builder /go/src/test/main .
CMD ["./main"]

Build

$ docker build -t zev/test:1.0.2 .         
                                                                                                   
Sending build context to Docker daemon  17.92kB
Step 1/14 : FROM golang:1.10.3 as builder
 ---> d0e7a411e3da
Step 2/14 : RUN apt-get update && apt-get install -y xz-utils     && rm -rf /var/lib/apt/lists/*
 ---> Running in 65772cb8fdab
Ign:1 http://deb.debian.org/debian stretch InRelease
Get:2 http://security.debian.org/debian-security stretch/updates InRelease [94.3 kB]
Get:3 http://deb.debian.org/debian stretch-updates InRelease [91.0 kB]
Get:4 http://security.debian.org/debian-security stretch/updates/main amd64 Packages [392 kB]
.....Omit here
Step 10/14 : RUN upx main
 ---> Running in d802406ee44a
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2018
UPX 3.95        Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
   9848136 ->   2945384   29.91%   linux/amd64   main

Packed 1 file.
Removing intermediate container d802406ee44a
 ---> 0c29f4b2272d
Step 11/14 : FROM alpine:3.8
 ---> 11cd0b38bc3c
Step 12/14 : WORKDIR /root
 ---> Using cache
 ---> ec68dc839562
Step 13/14 : COPY --from=builder /go/src/test/main .
 ---> a2c265cc9aff
Step 14/14 : CMD ["./main"]
 ---> Running in 7e350a4620ee
Removing intermediate container 7e350a4620ee
 ---> a4d7753c8112
Successfully built a4d7753c8112
Successfully tagged zev/test:1.0.2

Run

$ docker run -it -p 8080:8080 zev/test:1.0.2 

[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080

Images

$ docker images

REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
zev/test                                   1.0.2               a4d7753c8112        4 minutes ago       7.36MB

Now we have seen that the size of image has been reduced to 7.36MB, which is very small. In other words, our program is only 2.95M.

The size of alpine has been fixed. The starting point to make image smaller is only executable files. The main executable program can be compressed by using UPX shell technology, and the main volume can be reduced by 50%-70%.

 

 

 

Posted by ganesh on Tue, 14 May 2019 21:40:24 -0700