容器化的过程中总是免不了要构建镜像,一个体积更小的镜像除了能够节省机器的磁盘空间之外,还能够提升传输效率。这篇文章主要是想讲述一下自己在优化镜像体积时所采取的措施,当然并不是所有方案都对减少镜像体积有明显效果,具体项目还要具体分析。这篇文章我以 Rails 项目的镜像构建作为例子。
在优化镜像大小之前首先要知道为何我们所构建的镜像会这么大?下面是我项目中用于构建镜像的Dockerfile
文件
FROM ruby:2.5.3
RUN apt-get update -y && apt-get install -y \
build-essential \
imagemagick \
default-libmysqlclient-dev
RUN apt-get install -y \
nodejs \
yarn
RUN rm -rf /var/lib/apt/lists/*
WORKDIR /beansmile-web
COPY . /beansmile-web
RUN bundle install
镜像文件我定义得比较随意,它所构建出的镜像信息如下
web1 latest 1a8a32d5253a 9 hours ago 1.26GB
构建的镜像的过程跟平常基于一个操作系统打造供项目运行基础环境的过程差不多。只是日常的操作系统通常都不只一个项目在运行,因此系统里所包含的东西是比较全面的。而镜像只期望提供给特定的项目使用,因此所依赖的东西比较有针对性,不必要的东西尽量不要加进去。
针对上面的 Dockerfile 文件我觉得有以下几个优化方向
下面一条条来分析。
前面的例子最终构建出来的镜像体积十分庞大,主要归咎于相关的基础镜像本身就很大。
REPOSITORY TAG IMAGE ID CREATED SIZE
ruby 2.5.3 60c3a1518797 3 weeks ago 871MB
web1 latest 1a8a32d5253a 9 hours ago 1.26GB
可见我们的基础 Ruby 镜像本身就 800 多 M 了,构建镜像的过程还需要安装依赖,导致了最终的web
镜像体积会达到 1.26G。这个体积可不利于网络传输,官方所提供的Ruby 基础镜像有许多个版本,除了 Ruby 本身的版本不同之外,还有许多基于不同操作系统所构建的基础镜像可以选择,而这些不同的操作系统所构建出来的 Ruby 基础镜像的体积相差甚大
REPOSITORY TAG IMAGE ID CREATED SIZE
ruby 2.5.3-slim-stretch 20132a4ab93d 2 weeks ago 129MB
ruby 2.5.3 60c3a1518797 3 weeks ago 871MB
ruby 2.5.3-alpine b3361f13ff1f 3 weeks ago 43.6MB
基于alpine
操作系统的 Ruby 镜像是最迷你的,只有 43.6MB。slim-stretch
也是个不错的选择。或许采用更轻量级的镜像将会是一个优化的契机。
经验小贴士: 从我自己的构建经验来看,采用slim-stretch
或许会是更加亲民的选择,它是 Debian 系,包管理器跟ubuntu
是一样的都是用apt-get
,用惯ubuntu
的人肯定会觉得比较亲切。alpine
所用的包管理器是apk
(是不是想到安卓的安装包?),一些常用包的命名有点不太一样需要自己慢慢去解决。*
不过无论用哪种方案都避免不了时间的投入,网上也没那么多现成的解决方案,迷你镜像的话你不得不自己安装一些构建过程中所依赖的软件。
Docker 官网对镜像的说法是,它是由一层层的只读层组成的,层次越少镜像的性能表现越出众。这也是官方建议我们采用特定基础镜像去构建自己的项目镜像,而不是基于一个赤裸裸的操作系统镜像 (如 Ubuntu 镜像) 的原因。
上述的例子中我们用了三个RUN
命令,这会无意中多构建了两个层,其实我们可以把它合并成一条RUN
命令
RUN apt-get update -y && apt-get install -y \
build-essential \
imagemagick \
default-libmysqlclient-dev \
nodejs \
yarn \
&& rm -rf /var/lib/apt/lists/*
基于这个改动重新创建一个镜像web2
REPOSITORY TAG IMAGE ID CREATED SIZE
web2 latest 221a316a6903 14 minutes ago 1.25GB
web1 latest 1a8a32d5253a 9 hours ago 1.26GB
可见这种改动对于缩减镜像体积效果并不明显
官方的说法是这样的
In older versions of Docker, it was important that you minimized the number of layers in your images to ensure they were performant.
我们可以得出结论,或许缩减层数主要是为了让镜像操作起来更高效吧,减少层数这个优化方向对于缩减镜像体积并没有多大的帮助,不过我们这样做还是有好处的。
从上面的配置可以看出,为了方便镜像的构建我直接把整个项目都移动到镜像中去 (COPY
命令)。然而对于构建的镜像而言,并不是所有的文件我们都应该关心,最为值得关心的应该只有源码部分。所以我预想着在构建的镜像中可以把以下的目录剔除掉
PS: 当然每个人对实际项目的考量会有所不同,这几个目录只是根据我个人的项目情况所做的决定,并不具有通用性。
要忽略这些文件,我们采用一个名为.dockerignore
的文件,把它放在当前的目录下即可,它的写法跟.gitignore
文件很相似,内容大概如下
/public/**
/tmp/**
/log/**
然后重新构建镜像
web3 latest fb13cc1301b2 About a minute ago 1.2GB
web2 latest 221a316a6903 23 hours ago 1.25GB
web1 latest 1a8a32d5253a 33 hours ago 1.26GB
这种方式的影响也不怎么大,这是因为目前我本地这些目录下所包含的“垃圾”资源所占的比重较小。
这个是官方推荐的方案,在 Docker17.05 之后可以使用
In Docker 17.05 and higher, you can do multi-stage builds and only copy the artifacts you need into the final image. This allows you to include tools and debug information in your intermediate build stages without increasing the size of the final image.
好像看起来有点复杂,不过它的原理大概就是先使用一个体积较大,依赖较为齐全的镜像来构建所需要的资源,然后把这些资源复制到一个轻量的基础镜像中,并继续我们的镜像构建工作,这样就可以把原先庞大的基础镜像给抛弃了。这种做法能避免我们最终的镜像中包含了一堆无用的依赖,在某种程度上能够减少最终镜像的体积。
这看起来是个很不错的策略,我也在项目中进行了尝试。我们决定把 bundle 依赖包的安装以及,静态文件的编译都放到一个功能完备的基础镜像中去完成,然后把所需要的资源拷贝到一个轻量级的基础镜像中 (类似 alpine 这种轻量级系统的相关镜像) 再继续完成构建步骤。
不过我构建过程中遇到如下问题
bin/rails c
需要依赖 JS 运行时,这无论对于开发还是生产都是一个比较重要的操作,因此在最终镜像中舍弃 JS 运行时并不是个好主意。前面提到了 4 个优化的方向,但似乎最终只有
对最终的镜像体积影响较大。考虑到multi-stage
的解决方案所带来的好处可能还不如麻烦来得多,因此最终还是舍弃了这个方案,与其这样绕来绕去还不如直接采用最精简的ruby:2.5.3-alpine
作为基础镜像来打造自己的项目镜像。选择一个精简的操作系统最大的问题就是在构建项目镜像过程中的所有基础依赖都得自己一个个去解决,要投入不少的时间和精力,以下是我经过反复测试所得到的 Dockerfile 文件(仅供参考,毕竟你的项目所依赖的东西可能有所不同)
FROM ruby:2.5.3-alpine
RUN apk --update --upgrade add \
# bundle 安装相关的依赖
git \
curl \
# mysql2 依赖
mysql-dev \
# 基础设施,比如gcc相关的东西
build-base \
# nokogiri 相关依赖
libxslt-dev \
libxml2-dev \
# 图片处理相关依赖
imagemagick \
# tz相关,如果没有bundle的时候会报错
tzdata \
nodejs \
yarn \
&& rm -rf /var/cache/apk/*
WORKDIR /beansmile-web
COPY . /beansmile-web/
RUN bundle install
构建出来的镜像如下
web4 latest 71b75128d0d9 14 hours ago 586MB
与之前的镜像相比体积大幅度减少了。这是一个我们可以接受的大小了,考虑到时间成本就不进一步压缩了。
这篇文章主要简单总结了个人在缩减 Rails 项目镜像方面的探究。为了缩减镜像体积提出了 4 个主要的优化方向,用迷你的操作系统构建镜像的方式来减少镜像的体积的方式十分有效。不过不同类型,基于不同语言的项目可能会有不同的侧重点,不能一概而论,可能有的项目中multi-stage
会帮你省下更多的时间。