# 优化 Dockerfile，缩减镜像尺寸

原文：[Refactoring a Dockerfile for image size](https://blog.replicated.com/refactoring-a-dockerfile-for-image-size/)

Docker 社区最近有一篇关于镜像文件尺寸的[文章](http://www.iron.io/blog/2016/01/microcontainers-tiny-portable-containers.html)。[Docker](https://news.ycombinator.com/item?id=11000827) 和[社区](https://github.com/docker-library/golang/issues/77#issuecomment-172597368)都提倡更小的镜像。下面列出今天 Docker Hub 上的 Top 10 镜像的尺寸（ [Latest](https://medium.com/@mccode/the-misunderstood-docker-tag-latest-af3babfd6375#.nzttiqglh) ）：

|镜像名称|尺寸（MB）|
|---|---:|
|busybox|1 |
|ubuntu|188 |
|swarm|17|
|nginx|134|
|registry|423|
|redis|151|
|mysql|360|
|mongo|317|
|node|643|
|debian|125|

使用较小的基础镜像（例如 Alphine Linux, BusyBox 等）有很多好处。这方面有不少相关文档：

- [**Create the smallest possible docker container**](http://blog.xebia.com/create-the-smallest-possible-docker-container/)
- [**Docker Images**](http://blog.librato.com/posts/docker-images)
- [**Create super small docker images**](http://yasermartinez.com/blog/posts/creating-super-small-docker-images.html)
- [**Docker images base os size comparison**](https://www.brianchristner.io/docker-image-base-os-size-comparison/)

所以我假设你已经试用了其中的一个为基础镜像。接下来的事情就取决于操作者控制镜像尺寸的能力了。特别的，我们会验证一些缩减镜像大小的手段的效果，例如把多个 `RUN` 指令中的命令集中到一行，一些关于 `apt-get` 的技巧（例如移除 apt-get 缓存以及 `--no-install-recommends`）

## 在同一行中进行清理工作

Docker 镜像的基础是一种[层次化文件系统](https://en.wikipedia.org/wiki/Aufs)。每一层都只是包含同下面一层不同的部分。在最上方只能看到一个统一视图，而无法获知他的构建历史。Dockerfile 的每一行都会在顶部建立一个新的层。

例如我们以这样一个 Dockerfile 片段开始：

~~~dockerfile
ADD https://storage.googleapis.com/golang/go1.5.3.src.tar.gz /tmp

# do some things with that file

RUN rm /tmp/go1.5.3.src.tar.gz
~~~

你可以能会觉得删除 `.tar.gz` 文件是个负责任的好办法。但是包含这一文件的层还遗留在镜像之中。`rm` 操作只是对最终镜像隐藏了这一文件，使用 `docker pull` 获取这一镜像时还是会下载这些内容的。

更好的办法就是把这些操作集中到一行之中，这样就不会提交多个层了。例如，对上面的指令进行简单的改写：

~~~dockerfile
RUN curl -o \
        /tmp/go.1.5.3.src.tar.gz \
        https://storage.googleapis.com/golang/go1.5.3.src.tar.gz && \
      <do some things with the file> && \
      rm /tmp/go1.5.3.src.tar.gz
~~~

这样看起来有点丑，不过能够优化镜像尺寸。如果你讨厌这种写法，可以建立一个脚本，然后用 `ADD`、`RUN` 来在 Dokerfile 中运行。

## 用正确的方式移除 apt/yum 缓存

多数 Dockerfile 作者都知道应该 `apt-get remove` 所有不必要的包。一个例子就是，用 curl/wget 来进行下载安装其他软件。可以事后使用 `apt-get remove curl`，但是同上面的例子一样，curl 所在的层已经被封装到镜像之中，因此也应该把删除（包括相关的自动安装的依赖包）工作放到同一行中。

---

## 一个实际的例子。

这是一个简化版本的用于运行 Python 服务的 Dokerfile，我们将对他进行优化。

~~~dockerfile
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl python-pip

RUN pip install requests

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]
~~~

`myservice.py` 是一个 Python 脚本：

~~~python
#!/usr/bin/python
print 'Hello, world!'
~~~

接下来 build 并检查尺寸：

    $ sudo docker build -t size .
    $ sudo docker images
    REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
    size            latest        da8a9be731ac        4 seconds ago     360.5 MB
    ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

188 MB 的基础镜像已经很吓人了，不过更值得注意的是，只是一个 hello-world 的 Python 脚本为什么会导致镜像尺寸倍增。这 360.5 MB 中到底有什么？其中包含了最上一层（本例中是 da8a9be731ac），以及用于建立顶层的所有的层的内容。

### 添加一个清理层

我们也许应该做一些清理工作，我们试试这样的 Dockerfile ：

~~~dockerfile
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl python-pip

RUN pip install requests

## Clean up
RUN apt-get remove -y python-pip curl
RUN rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]
~~~

Build，然后再看尺寸：

    $ sudo docker build -t size .
    $ sudo docker images
    REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
    size            latest        c6dacdd00660        2 seconds ago     361.3 MB
    ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

事与愿违，镜像更大了。

### 在同一层进行清理

接下来试试把 APT 操作进行合并：

~~~dockerfile
FROM ubuntu:14.04
RUN apt-get update && \
    apt-get install -y curl python-pip && \
    pip install requests && \
    apt-get remove -y python-pip curl && \
    rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]
~~~

Build 然后查看镜像：

    $ sudo docker build -t size .
    $ sudo docker images
    REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
    size            latest        e531f8674f33        9 seconds ago     338 MB
    ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

果然小了一点点 —— 只是一点点。

### 进一步优化 APT

`apt-get install` 过程中加入了很多 `推荐` 内容。[推荐包](http://unix.stackexchange.com/questions/77053/apt-installing-more-packages-than-specified-as-dependencies)只是简单的加入了一些可有可无的依赖。有些用户会因为特殊的应用方式，或者环境要求才需要这些东西，换句话说，这些推荐内容并非必要。

在 Ubuntu 14.04 中运行 PIP，很容易就可以得出结论：删除推荐包并不会有什么副作用。这一点在产品发布之前是完全可以确认的。可以在 Docker Hub 上看看 [redis](https://github.com/docker-library/redis/blob/b375650fb69b7db819e90c0033433c705b28656e/3.0/Dockerfile#L6)，[mysql](https://github.com/docker-library/mysql/blob/ed198ce2e8aa78613c615f20c5c4dd09fa450f66/5.7/Dockerfile#L13)，[mongo](https://github.com/docker-library/mongo/blob/7da0e6d6520607c99d40cf71a2e4b0a2da0beca9/3.2/Dockerfile#L7)，[postgres](https://github.com/docker-library/postgres/blob/916a840510b481e7d3f0f74fa04fde3edfdfbd04/9.5/Dockerfile#L9)，[elasticsearch](https://github.com/docker-library/elasticsearch/blob/5b2bf54e2c17a8e2e1b062ea0d071eae600bfec2/2.2/Dockerfile#L23) 等的镜像，使用这一技巧来缩减镜像尺寸。

实际操作一下带有 `--no-install-recommends` 选项的 APT-GET：

~~~dockerfile
FROM ubuntu:14.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl python-pip && \
    pip install requests && \
    apt-get remove -y python-pip curl && \
    rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]
~~~

Build，再次检查尺寸：

    REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
    size            latest        fddc30aee4dc        6 seconds ago     229.2 MB
    ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

这次我们把镜像缩小了 120 MB。

---

Dockerfile 的语法非常简单，也同样有优化的需求，因此在组织中应用 Docker 时，需要为 Dockerfile 建立健全相应的策略来优化这一过程。
