缩减Docker镜像体积历程总结

容器化的过程当中老是免不了要构建镜像,一个体积更小的镜像除了可以节省机器的磁盘空间以外,还可以提高传输效率。这篇文章主要是想讲述一下本身在优化镜像体积时所采起的措施,固然并非全部方案都对减小镜像体积有明显效果,具体项目还要具体分析。这篇文章我以Rails项目的镜像构建做为例子。node

为何构建出来的镜像这么大?

在优化镜像大小以前首先要知道为什么咱们所构建的镜像会这么大?下面是我项目中用于构建镜像的Dockerfile文件mysql

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
复制代码

镜像文件我定义得比较随意,它所构建出的镜像信息以下git

web1                 latest               1a8a32d5253a        9 hours ago         1.26GB
复制代码

构建的镜像的过程跟日常基于一个操做系统打造供项目运行基础环境的过程差很少。只是平常的操做系统一般都不仅一个项目在运行,所以系统里所包含的东西是比较全面的。而镜像只指望提供给特定的项目使用,所以所依赖的东西比较有针对性,没必要要的东西尽可能不要加进去。github

针对上面的Dockerfile文件我以为有如下几个优化方向web

  1. 基础的ruby:2.5.3是基于buildpack-deps来构建的其中包含大量额外的软件包,或许用更轻量级的镜像来做为基础镜像可以进一步缩减空间。
  2. 文件中有太多的RUN命令,每一条命令都会叠加层数,可能会形成体积的变大。
  3. 把整个项目目录都拷贝到镜像中,并非个好主意,随着项目的增大,Rails项目中的public目录下可能会存在着图片之类的静态资源,把这些东西打包到镜像中意义不大。
  4. 是否可以采用multi-stage的构建方式,把一部分不那么紧要的资源丢弃,让最终镜像体积更小?

下面一条条来分析。sql

具体分析

方向1: 基于更小的操做系统

前面的例子最终构建出来的镜像体积十分庞大,主要归咎于相关的基础镜像自己就很大。docker

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基础镜像的体积相差甚大ubuntu

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(是否是想到安卓的安装包?),一些经常使用包的命名有点不太同样须要本身慢慢去解决。*ruby

不过不管用哪一种方案都避免不了时间的投入,网上也没那么多现成的解决方案,迷你镜像的话你不得不本身安装一些构建过程当中所依赖的软件。

方向2: 缩减镜像的层数

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.

咱们能够得出结论,或许缩减层数主要是为了让镜像操做起来更高效吧,减小层数这个优化方向对于缩减镜像体积并无多大的帮助,不过咱们这样作仍是有好处的。

方向3: 忽略一些文件

从上面的配置能够看出,为了方便镜像的构建我直接把整个项目都移动到镜像中去(COPY命令)。然而对于构建的镜像而言,并非全部的文件咱们都应该关心,最为值得关心的应该只有源码部分。因此我预想着在构建的镜像中能够把如下的目录剔除掉

  • public/: 用于存放一些静态文件的目录,若是其中包含大量像图片这样的资源的话会对镜像的体积有较大的影响。
  • tmp/: 用于存放一些缓存资源,项目进程文件等等,这些文件对于镜像而言用处不大。
  • log/: 用于存放日志相关的信息。

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
复制代码

这种方式的影响也不怎么大,这是由于目前我本地这些目录下所包含的“垃圾”资源所占的比重较小。

方向4: multi-stage方案

这个是官方推荐的方案,在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这种轻量级系统的相关镜像)再继续完成构建步骤。

不过我构建过程当中遇到以下问题

  • 用bundle安装依赖的过程当中不只仅涉及到ruby代码的引入,mysql2nokogiri这些第三方库除了会引入Ruby代码以外还会在安装的时候进行编译,并生成一些共享库,若是把依赖资源从一个镜像拷贝到另外一个镜像的话除了要拷贝bundle相关目录下的ruby代码以外,还不得不拷贝这些第三方库所依赖的共享库,这比想象中要麻烦。
  • 咱们指望在一个镜像里面完成静态文件的构建,那么咱们即可以在最终镜像中免去了安装nodejs, yarn这些用于编译静态资源相关的依赖了。不事后来仍是以为这种方案不太适用。一方面,安装了nodejs与没有安装nodejs的镜像差异也就是30M左右,另外一方面,要运行bin/rails c须要依赖JS运行时,这不管对于开发仍是生产都是一个比较重要的操做,所以在最终镜像中舍弃JS运行时并非个好主意。

最终构建

前面提到了4个优化的方向,但彷佛最终只有

  • 采用更轻量级的操做系统的相关基础镜像来进行构建。
  • multi-stage。

对最终的镜像体积影响较大。考虑到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会帮你省下更多的时间。

相关文章
相关标签/搜索