最近遇到了 Docker 镜像体积过大的问题,对于部署、移交相当麻烦和慢。于是就抽点时间研究下了怎么减小 Docker 镜像的体积。

下面我以手动编译 nginx 镜像作为例子来减小镜像体积,nginx 的版本是 1.10.2,并且下载到了当前目录。

下图是各种 Dockerfile 制作出镜像的效果

# 替换基础镜像 我们的基础镜像是基于 centos 7.X,从官方 hub 拉取的 centos 7 的基础镜像有 194.6MB,这个基础镜像已经相当的大了,所以减小镜像体积的第一步是替换基础镜像。首选的基础镜像是 alpine,这里我选择的是 3.4。
##基于 centos 的镜像 下面是最原始的镜像的 Dockerfile
FROM centos:7.2.1511
COPY . /tmp/

RUN yum install -y make \
    gcc \
    openssl-devel \
    zlib-devel \
    perl-devel \
    pcre-devel && \
    cd /tmp/ && tar zxf nginx-1.10.2.tar.gz && \
    cd /tmp/nginx-1.10.2 && \
    ./configure --with-http_gzip_static_module --with-http_ssl_module && \
    make && make install && \
    rm -rf /tmp/* && \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log && \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

上述就是一个最基本的 nginx 镜像了,通过 docker build 后,镜像大小是 535.2MB。nginx 的源代码才 890KB,这种镜像,完全是浪费空间。

## 基于 Alpine 的镜像 对于 centos 的基础镜像,本身就已经接近 200MB 了,然后 Alpine 的基础镜像才 4.999MB,下面是改为 Alpine 的基础镜像的 Dockerfile
FROM alpine:3.4
COPY . /tmp/

RUN apk update && \
    apk add gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev && \
    cd /tmp/ && tar zxf nginx-1.10.2.tar.gz && \
    cd /tmp/nginx-1.10.2 && \
    ./configure --with-http_gzip_static_module --with-http_ssl_module && \
    make && make install && \
    rm -rf /tmp/* && \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log && \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

通过上述的 Dockerfile docker build 出来的镜像大小是 155.8MB,体积已经缩小到了 3 倍以上了,对于 nginx 的运行来说,centos 和 Alpine 完全是一样的效果,所以对于基础镜像,首选应该是 Alpine 这之类的精简版系统,而且在官方 hub 上的官方镜像,大多有提供 Alpine 版的版本。

# 移除 build 依赖的文件 有一些软件安装时,可能在编译时会依赖于一些头文件,例如 `openssl-dev` 这之类的 `dev` 文件,还有就是 `gcc` 和 `make` 这之类的编译工具,在程序运行时完全不需要,所以可以在编译完之后,删除掉,这样可以也是可以减小一定的体积。
## centos 基础镜像的依赖 ```Dockerfile FROM centos:7.2.1511

COPY . /tmp/

RUN yum install -y make
gcc
openssl-devel
zlib-devel
perl-devel
pcre-devel &&
cd /tmp/ && tar zxf nginx-1.10.2.tar.gz &&
cd /tmp/nginx-1.10.2 &&
./configure --with-http_gzip_static_module --with-http_ssl_module &&
make && make install &&
yum remove -y gcc openssl-devel zlib-devel perl-devel pcre-devel make &&
yum clean all &&
rm -rf /tmp/* &&
ln -sf /dev/stdout /usr/local/nginx/logs/access.log &&
ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

上面的 `yum remove -y gcc openssl-devel zlib-devel perl-devel pcre-devel make` 和 `yum clean all` 就是移除不需要的文件,从而进一步减小体积,加上两条命令后,执行 `docker build` 后的镜像的体积为 413.8MB,相对上述的 535.2MB 减小了 121.4 MB。

<div id="build-dependence-file-alpine"></div>
## Alpine 基础镜像的依赖
上述 centos 的移除依赖的后体积变化可能看着没有啥感觉,因为本身就很大,下面是 Alpine 的 Dockerfile

```Dockerfile
FROM alpine:3.4

COPY . /tmp/

RUN apk update && \
    apk add gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev && \
    cd /tmp/ && tar zxf nginx-1.10.2.tar.gz && \
    cd /tmp/nginx-1.10.2 && \
    ./configure --with-http_gzip_static_module --with-http_ssl_module && \
    make && make install && \
    apk del gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev && \
    rm -rf /var/cache/* && \
    rm -rf /tmp/* && \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log && \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

这次 docker build 出来的镜像的体积为 11.74MB,直接从 155.8MB 减小了 144.04 MB,达到了数量级的变化。并且这个 11.74MB 的镜像也是可以正常运行的。

# 减少镜像层级 由于 Docker 镜像的储存原理是分层的,其实 `docker build` 的过程就是 docker 运行了一个容器,然后执行 Dockerfile 里写的命令。并且每一个命令都会 commit 一下,每一次 commit 都是一层一层的叠加在原来的镜像上,也就是说在某一层里增加了一个文件,在下一层里删除这个文件,是没有任何效果的,镜像体积是不变的,可能反而会增加。所以减小镜像的体积除了替换基础镜像,还需要优化 Dockerfile,减少镜像的层级。

那么对于一些需要通过 COPY 命令的方式拷贝到镜像里面的文件,可以使用 wget 的方式,下载到镜像里,然后使用完之后就删掉。

## centos ```Dockerfile FROM centos:7.2.1511

RUN yum install -y make
gcc
openssl-devel
zlib-devel
perl-devel
pcre-devel
wget &&
wget --no-check-certificate -O /tmp/nginx-1.10.2.tar.gz http://nginx.org/download/nginx-1.10.2.tar.gz &&
cd /tmp/ && tar zxf nginx-1.10.2.tar.gz &&
cd /tmp/nginx-1.10.2 &&
./configure --with-http_gzip_static_module --with-http_ssl_module &&
make && make install &&
rm -rf /tmp/* &&
ln -sf /dev/stdout /usr/local/nginx/logs/access.log &&
ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

上述通过 wget 的下载方式将 nginx 的源代码下载到了镜像里面,然后使用完之后,再删除掉,这样对于镜像来说,将不会存在 nginx 的源代码。通过此方法 `docker build` 出来的镜像是 534.8MB,相对于最原始的 535.2MB,少了一丢丢,这种方式随着文件的大小增大,效果显著。

<div id="level-alpine"></div>
## Alpine
```Dockerfile
FROM alpine:3.4

RUN apk update && \
    apk add gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev wget && \
    wget --no-check-certificate -O /tmp/nginx-1.10.2.tar.gz http://nginx.org/download/nginx-1.10.2.tar.gz && \
    cd /tmp/ && tar zxf nginx-1.10.2.tar.gz && \
    cd /tmp/nginx-1.10.2 && \
    ./configure --with-http_gzip_static_module --with-http_ssl_module && \
    make && make install && \
    rm -rf /tmp/* && \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log && \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

通过 docker build 后,镜像大小为 155.4MB,因为 Alpine 的体积本身很小,所以效果不明显。

## 大文件测试 为了测试大文件的效果,我通过 dd 生成了一个 2.3GB 的文件,然后我分别通过 copy 的方式和 wget 的方式拷贝文件到镜像,并且移除文件。
### 通过 COPY ```Dockerfile FROM alpine:3.4

COPY . /code
RUN rm -rf /code

CMD ["tail", "-f", "/etc/hosts"]

基础镜像采用的是 Alpine,对于 2.3GB 来说,基础的镜像的体积可以忽略。`docker build` 出来的镜像体积是 2.463GB,可以看到,虽然通过 `rm -rf /code` 将文件删除了,但是由于 docker 镜像是分层的原因,所以镜像的依旧包含了 COPY 进去的文件的体积,虽然文件在 `docker run` 时是看不到的。

<div id="bigfile-by-wget"></div>
### 通过 wget
```Dockerfile
FROM alpine:3.4

RUN apk update && apk add wget && \
    mkdir /code && \
    wget --no-check-certificate -O /code/test.dbf http://172.24.4.188:8000/test.dbf && \
    apk del wget && \
    rm -rf /var/cache/* && \
    rm -rf /code

CMD ["tail", "-f", "/etc/hosts"]

通过此方法 docker build 出来的镜像体积为 4.817MB,相对于 Alpine 的基础镜像 4.799MB,只增加了 0.018MB。相对于上述的 COPY 完全是质的变化。

#其他
## Python 对于 Python 来说,在制作镜像时,往往会预先安装依赖的库,在安装的过程中,往往会产生大量的 `pyc` 和 `pyo` 文件,这些文件对于镜像来说完全是不需要的,完全可以在镜像成为容器运行时生成,当然,这可能会影响一丢丢启动速度,毕竟文件是需要生成的。所以以时间换空间的话,完全可以移除掉。

通过 find / -name "*.py[co]" -exec rm '{}' ';' 就可以全都移除掉

## 通过镜像层级加速 build 过程 有时,在开发的时候,需要频繁的 build 镜像,如果全部在一个 RUN 里来执行,那么每次 build 时,所有的依赖都会安装一遍,这样极其浪费时间,可以通过 docker 镜像分层的原理,将安装依赖的过程放在一层里,然后代码放在最后 COPY 进去,因为一般情况下,都是代码频繁的变动,依赖的软件不会经常变动,这样可以利用 build 时的 cache,加速 build 的过程。