Lan Tian @ Blog

I'm starting to provide Chinese / English versions of some articles, switch with the Language menu above. 我开始提供部分文章的中文、英文翻译,请使用顶部语言菜单切换。

Docker 镜像的精简

自从放弃 OpenVZ 架构的 VPS 并购买 KVM 架构的 VPS 以来,我一直使用 Docker 部署 nginx、MariaDB、PHP 等网站需要的程序,不仅方便平时单个服务的重启和配置管理(把配置目录全部用 volume 映射到一起管理),而且方便了服务的升级。

例如,我现在博客所在的 VPS 因为配置不高,内存占用最近一直在 80% 左右。我要更新 nginx 或者向 nginx 里加模块时,如果直接在这台 VPS 上编译 nginx,不仅速度慢,而且有可能因为内存不足而把网站也崩掉。使用 Docker 后,我就可以在其它的空闲资源较多的 VPS 或者在我自己的电脑上构建镜像,push 到 Docker Hub,再在 VPS 上 pull 下来运行。

不过,一直以来,我的 nginx 镜像大小都在 200 MB 左右(Docker Hub 显示大小,不包含基础镜像大小,比一般 docker image 显示的要小),明显大于它应该有的容量,不过因为 VPS 的硬盘暂时足够,而且前段时间我也没什么时间,我就没有管这个问题。现在有了空,我就仔细研究并解决了这个问题,修改了 Dockerfile,把镜像大小降到了 17 MB。

合并 RUN 命令

刚打开 Dockerfile,我就发现了一个低级错误:

FROM debian:stretch
MAINTAINER Lan Tian "lantian@lantian.pub"
ENV NGINX_VERSION=1.15.1 OPENSSL_VERSION=OpenSSL_1_1_1-pre8
RUN apt-get update -q \
    && apt-get -y upgrade \
    && apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev patch
RUN cd /tmp \
    && wget -q http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
    && tar xf nginx-${NGINX_VERSION}.tar.gz \
    && # 接下来是编译过程

Docker 在构建镜像时,会为 Dockerfile 中的每一条指令生成一“层”镜像,将一层层镜像叠加起来成为一“个”镜像。

在上述第一条 RUN 指令中,Docker 生成了一“层”包含了 nginx 所需的依赖包,以及 apt-get 下载下来的临时文件的镜像。后续的编译、安装过程在第二条 RUN 指令中,是在含依赖包那一层之上的一“层”操作的,完全不能影响到依赖包那一层。虽然我后续执行了 apt-get clean,但是这只是在最顶层中标记了删除这些文件,这些文件仍然在第二层含依赖包的一层留存着。

修改方法就是把两个 RUN 命令通过 && 和反斜杠连接起来,共同构成一层就可以了。

当时我这样写 Dockerfile,是因为后续的 nginx 编译过程反复出现问题,我在测试时为了防止反复下载依赖包就分离了这些指令,来重复利用下载好的依赖包。但是在测试完成后,我忘了把这里改回去!

这里修改后,Docker 镜像的大小直接小了 25 MB。虽然 175 MB 的镜像比之前已经小了一些,但是还是很大,还有精简的空间

使用 Alpine 作为基础镜像

我之前一直使用我比较熟悉的 Debian 作为基础镜像。Debian 的好处是可以使用常用的 apt-get 等工具,但是它基础镜像的体积大概在 125 MB 左右,很大。而 Alpine Linux 是一个体积只有 5 MB 左右,被广泛运用在 Docker 镜像中的 Linux 发行版。为了精简体积,我准备使用它。

首先是把 Dockerfile 开头的 FROM debian:stretch 换成 FROM alpine。我直接用了 Alpine 的最新版本,如果软件对系统版本有要求记得指定。

然后,把 apt-get 安装的依赖包在 Alpine 的包管理器 apk 中找到对应的包,并安装上去。我在 Debian 中安装依赖的命令如下:

apt-get update -q
apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev patch

把这些包一一在 apk 中找到对应的包:

apk --no-cache add build-base git autoconf automake libtool wget tar zlib-dev pcre-dev unzip jemalloc-dev patch linux-headers

其中 --no-cache 参数代表 apk 不使用本地缓存,直接去镜像源拉取软件包列表和下载,并且会自动删除下载缓存,相当于自带了 apt-get update 和 apt-get clean 两行。

同理,把最后删除编译依赖包的部分也改掉。原先在 Debian 下我是这样删的:

apt-get purge -y unzip -q git autoconf libtool automake build-essential
apt-get autoremove -y --purge

在 Alpine 下我这样删:

apk del build-base git autoconf automake wget tar unzip patch linux-headers

这样操作后,镜像的体积被直接减到了 17 MB!在 docker images 中包含 Alpine 本身显示为 39.6 MB。相比原先已经非常小了。这里应该不只是 Alpine 本身的差距,还可能和 Alpine 自带的轻量级 musl C 库文件有关。

同样操作精简其它镜像

nginx 的镜像精简完了,用类似的操作也可以精简其它的镜像。首先拿 PHP-FPM 开刀,这里因为我需要一大堆 PHP 模块所以不能直接用官方的镜像。

原先的安装命令是这样,我的 PHP-FPM 不是编译的,是直接从软件源安装的:

apt-get install -y php7.2-fpm php7.2-bz2 php7.2-curl php7.2-gd php7.2-json php7.2-mbstring php7.2-memcached php7.2-mysql php7.2-redis php7.2-sqlite3 php7.2-xml php7.2-xmlrpc php7.2-zip php7.2-intl

Alpine 的软件源也有 PHP-FPM,可以直接安装:

apk --no-cache add php7-fpm php7-bz2 php7-curl php7-gd php7-json php7-mbstring php7-memcached php7-mysqli php7-pdo_mysql php7-redis php7-sqlite3 php7-pdo_sqlite php7-xml php7-xmlrpc php7-zip php7-intl php7-ctype php7-tokenizer

其中注意 Alpine 软件源的 PHP 有点迷,默认不包含一些在其它发行版上有的包,例如 php7-ctype、php7-tokenizer,如果不装,Typecho 等程序就会报错。

换成 Alpine 后 Docker Hub 上显示的大小从 90 MB 降到了 25 MB,docker image 中的大小降到了原先的 1/4。

MariaDB 我也换成了基于 Alpine 的,不过这个镜像因为有现成的,我就没自己做,直接用了 bianjp/mariadb-alpine。它和官方基于 Debian 的镜像几乎完全兼容,我在 docker-compose.yml 里只改了 image 名就成功了,什么设置都不用动。

总结

这样操作后我的 VPS 里省了大概 1 GB 的空间,对于我的小硬盘 VPS 来说已经省出了很多的空间了。

而且以后更新程序时 push、pull 镜像的速度也大大加快了,尤其是香港 VPS 从 Docker Hub pull 镜像的速度一直不怎么样,更小的镜像意味着在更短的时间内可以 pull 下更多个版本进行测试,出现问题 pull 原来版本镜像恢复运行的速度也会更快。