Originally published on Kuricat.com

[TOC]

I didn’t care about the size of Docker images until now…

(Maybe the subtitle could have been how to Reduce the size of your Docker Image by 99%!!XD: Joy: Shame 233)

origin

After writing Dockerfile to build the image and running it in Kubernetes, IT was found that the disk capacity of Worker Node was consumed quickly. It was found that the Docker Container in Pod was too large. A container just for running micro/ Micro’s Micro Api takes up 1G+…. , which made people feel ashamed, so I began to try to slim down the Image of the container.

First attempt (delete remaining cache)

As shown in the DockerFile below without any cleanup, let’s see how much space the Image takes up after the build

FROM alpine:latest

USER root

RUN apk update && apk add go git musl-dev

Add the go environment variable
RUN echo "export PATH=\$PATH:/root/go/bin" >> /etc/profile \
    && echo "export GO111MODULE=on" >> /etc/profile \
    && echo "export GOPATH=/root/go" >> /etc/profile  \
    && echo "export GOPROXY=https://mirrors.aliyun.com/goproxy/" >> /etc/profile \
    && source /etc/profile

# to install the micro
RUN cd \
    && go get -u -v github.com/micro/micro \
    && go install github.com/micro/micro

EXPOSE 8080

CMD ["micro"."api"]
Copy the code

Build the Docker Image

$docker build. --tag=micro:v1.01# Check the mirror size2 $docker image ls | grep micro micro v1.01 b1330a05f90 27 seconds line 948 MBCopy the code

Ok, the resulting image takes up 948 MEgabytes of space, which is close to 1 gigabyte in volume. However, the binaries compiled by Go are only tens of megabytes at most, so there must be some unnecessary occupation, so let’s go into the container to see where the main space occupation is concentrated

$docker run it --rm micro:v1.01 /bin/sh# Because busybox has an earlier du command, ncDU is used here
/  apk add ncdu
/  ncdu
Copy the code

And then we can observe that the size is mainly concentrated in

  • /root/go/src 471+MB // go Dependency files downloaded during compilation
  • /root/go/bin 36+MB // micro Compiles out the binary file we need
  • /usr/lib320 +MB //
  • /usr/libexec 66+MB
  • .

For a brief analysis, mainly the software and lib installed by our apk command above, and the dependencies required by Go to compile Micro.

In fact, we compile Micro binary files that run without any of this.

So let’s modify the Dockerfile above, delete all apK installed software and Go dependency, in order to reduce the image size

FROM alpine:latest

USER root

Add the go environment variable
RUN echo "export PATH=\$PATH:/root/go/bin" >> /etc/profile \
    && echo "export GO111MODULE=on" >> /etc/profile \
    && echo "export GOPATH=/root/go" >> /etc/profile \
    && echo "export GOPROXY=https://mirrors.aliyun.com/goproxy/" >> /etc/profile \
    && source /etc/profile

# to install the micro
RUN apk update && apk add go git musl-dev \
    && cd \
    && go get -u -v github.com/micro/micro \
    && go install github.com/micro/micro \
    && apk del go git musl-dev \
    ; rm /root/go/pkg -rf \
    ; rm /root/go/src -rf \
    ; rm /root/.cache -rf \
    ; rm -rf /var/cache/apk/* /tmp/*

EXPOSE 8080

CMD ["micro"."api"]
Copy the code

Let’s build and see what happens.

$docker build. --tag=micro:v1.02# Check the mirror size$docker image ls | grep micro micro v1.02 03 e65f4ba3ed 8 seconds line 44.1 MBCopy the code

This time the optimization effect is remarkable, after the build image size of 44.1MB, only 4.6% of the original! Phase one is complete!

PS here has a small episode, the first optimization of the time silly…. I wrote my Dockerfile like this…

FROM alpine:latest

USER root

Install the software environment
RUN apk update && apk add go git musl-dev

# Add the go environment variable and install micro
RUN echo "export PATH=\$PATH:/root/go/bin" >> /etc/profile \
    && echo "export GO111MODULE=on" >> /etc/profile \
    && echo "export GOPATH=/root/go" >> /etc/profile \
    && echo "export GOPROXY=https://mirrors.aliyun.com/goproxy/" >> /etc/profile \
    && source /etc/profile \
    && go get -u -v github.com/micro/micro \
    && go install github.com/micro/micro \

# Remove the unnecessary
RUN cd \
    &&
    && apk del go git musl-dev \
    ; rm /root/go/pkg -rf \
    ; rm /root/go/src -rf \
    ; rm /root/.cache -rf \
    ; rm -rf /var/cache/apk/* /tmp/*

EXPOSE 8080

CMD ["micro"."api"]
Copy the code

At first glance, it may seem fine and logical, but after the build, the mirror size does not shrink at all, so what’s the problem?

And the problem is this clarity: Joy: it’s a combination of two things:

  1. Docker uses AUFS, or federated file systems
  2. In Dockerfile, a command is a layer

In this Dockerfile, we want to delete the contents of the upper layer from the lower layer, which is not possible: Joy:

Second attempt (multi-phase build)

In the previous phase, we controlled the mirror volume to 44+MB, which worked well. But there is a troubling and inelegant aspect. Every time we write a Dockerfile, do we have to manually delete the installed stuff? !!!!! .

The answer is no.

After Docker 17.05, Docker supports multistage builds, in which the container only stores content built in the last stage and we can write anything in the previous stage. Install ten software and write eleven Run apk add foo :joy:(joking). So we changed the Dockerfile to look like this.

FROM alpine:latest

USER root

# Add the Go environment variable and alpine mirror
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
    && echo "export GO111MODULE=on" >> /etc/profile \
    && echo "export GOPATH=/root/go" >> /etc/profile \
    && echo "export GOPROXY=https://mirrors.aliyun.com/goproxy/" >> /etc/profile \
    && source /etc/profile

# to install the micro
RUN apk update && apk add go git musl-dev xz binutils \
    && cd \
    && go get -u -v github.com/micro/micro \
    && go install github.com/micro/micro


# 1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
FROM alpine:latest

COPY --from=0 /root/go/bin/micro /usr/local/bin/

EXPOSE 8080

CMD ["micro"."api"]
Copy the code

Let’s build an image to verify

$docker build. --tag=micro:v1.03# Check the mirror size8529 $docker image ls | grep micro micro v1.03 f0d7aaca 7 seconds line 43.7 MBCopy the code

As you can see, the container is about the same size, or even smaller.

This can basically achieve our expectations, but there is no way to become smaller, of course, there are!!!!

The following approach is for compiled languages like Go, while dynamic languages like PHP/Python may need other ideas.

Third attempt (reduce binary size)

A Google search revealed two tools: Strip and UPX.

Here’s a quick look at the Strip and upX

Strip reduces the size of programs by removing the TYPchk segment, symbol table, string table, line number information, debug segment, comment segment, and relocation information of ELF headers in executable files. In short, you can literally say undress the program…

UPX is originally a BIN file packer, but it has a compression algorithm called UCL, which can further reduce the size, decompress in memory first, very small impact on performance (side effect is to increase the program startup time, recommended caution)

Next we apply strip + upx to our Dockerfile.

FROM alpine:latest

USER root

# Add the Go environment variable and alpine mirror
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
    && echo "export GO111MODULE=on" >> /etc/profile \
    && echo "export GOPATH=/root/go" >> /etc/profile \
    && echo "export GOPROXY=https://mirrors.aliyun.com/goproxy/" >> /etc/profile \
    && source /etc/profile

# to install the micro
RUN apk update && apk add go git musl-dev xz binutils \
    && cd \
    && go get -u -v github.com/micro/micro \
    && go install github.com/micro/micro

# Compress and shell
RUNWget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz \ && xz-dUpx-3.95-amd64_linux.tar.xz \ && tar-xvf UPx-3.95-amd64_linux.tar \ &&cdUpx-3.95-amd64_linux \ && chmod a+x./upx \ && mv./upx /usr/local/bin/ \
    && cd /root/go/bin \
    && strip --strip-unneeded micro \
    && upx micro \
    && chmod a+x ./micro \
    && cp micro /usr/local/bin

# 1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
FROM alpine:latest

COPY --from=0 /usr/local/bin/micro /usr/local/bin/

EXPOSE 8080

CMD ["micro"."api"]
Copy the code

Let’s build the image to see what it looks like

$docker build. --tag=micro:v1.04# Check the mirror size$docker image ls | grep micro micro v1.04 1 d22ac38352c 5 seconds line 14.2 MBCopy the code

The size of the image is further reduced to 14.2 MB, so let’s go into the container and look at the compiled micro binary file size,

$docker run it --rm micro:v1.04 /bin/sh/ls -hal /usr/local/bin/micro
-rwxr-xr-x    1    root    root    8.2M    Aug 12 07:59    micro
Copy the code

We’re down to 8.2 meters, so let’s do the math, and all that extra volume.

Use the Docker Image command to see the size of Alpine, our base image

$docker image ls | grep alpine alpine latest b7b28af77ffe five weekes line 5.58 MBCopy the code

8.2 + 5.58 = 13.78 MB, that is, there is 14.2-13.78 = 0.42 MB of excess occupation, we are almost to the extreme. This is basically the end of optimization.

Postscript. – Unfinished business

So far, we have reduced the size of the container image from 948MB to 14+MB, reducing it by nearly 98.5% :joy:.

However, we also see that there is 0.42MB of excess occupation waiting for us to optimize, which may be due to calculation errors or insufficient understanding of Docker multi-stage construction and AUFS, leading to the perception that there is excess occupation space, which is an unfinished matter.

I want to be like Docker mirror image, so thin so fast duck: Joy 🙂

Originally published on Kuricat.com

Welcome to kuri-su /KBlog for an Issue discussion