减小 Docker 镜像体积

最近遇到了 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 文件,还有就是 gccmake 这之类的编译工具,在程序运行时完全不需要,所以可以在编译完之后,删除掉,这样可以也是可以减小一定的体积。

centos 基础镜像的依赖

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 makeyum clean all 就是移除不需要的文件,从而进一步减小体积,加上两条命令后,执行 docker build 后的镜像的体积为 413.8MB,相对上述的 535.2MB 减小了 121.4 MB。

Alpine 基础镜像的依赖

上述 centos 的移除依赖的后体积变化可能看着没有啥感觉,因为本身就很大,下面是 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 && \
    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

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,少了一丢丢,这种方式随着文件的大小增大,效果显著。

Alpine

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

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 时是看不到的。

通过 wget

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

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

通过镜像层级加速 build 过程

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