前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >写DockerFile的一些技巧

写DockerFile的一些技巧

原创
作者头像
iginkgo18
修改2021-06-28 14:35:33
9580
修改2021-06-28 14:35:33
举报
文章被收录于专栏:devops_k8sdevops_k8s

Docker镜像由只读层组成,每个层都代表一个Dockerfile指令。这些层是堆叠的,每一层都是前一层变化的增量。示例Dockerfile:

代码语言:javascript
复制
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

每条指令创建一个层:

FROM从ubuntu:15.04Docker镜像创建一个图层。

COPY 从Docker客户端的当前目录添加文件。

RUN用你的应用程序构建make。

CMD 指定在容器中运行的命令。

运行图像并生成容器时,可以 在基础图层的顶部添加新的可写层(“容器图层”)。对正在运行的容器所做的所有更改(例如写入新文件,修改现有文件和删除文件)都将写入此可写容器层。

使用标签

给镜像打上标签, 易读的镜像标签可以帮助了解镜像的功能。

使用统一的Base镜像

比如busybox或者alpine,谨慎选择基础镜像,尽量选择当前官方的镜像库中镜像;

很多教程中建议大家使用alpine镜像,更建议大家使用centos,Ubuntu这样的镜像。同时,在构建自己的Docker镜像时,只安装和更新必须使用的包,FROM指令应该包含的参数tag,比如使用centos:7.5.1504而不是FROM centos。

充分利用缓存

在镜像的构建过程中,Docker 会遍历 Dockerfile 文件中的指令,然后按顺序执行。在执行每条指令之前,Docker 都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建。如果你不想在构建过程中使用缓存,你可以在 docker build 命令中使用 --no-cache=true 选项;

但是,如果你想在构建的过程中使用缓存,你得明白什么时候会,什么时候不会找到匹配的镜像,遵循的基本规则如下:

  • 从一个基础镜像开始(FROM 指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。如果不是,则缓存失效。
  • 在大多数情况下,只需要简单地对比 Dockerfile 中的指令和子镜像。然而,有些指令需要更多的检查和解释。
  • 对于 ADD 和 COPY 指令,镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验和。文件的最后修改时间和最后访问时间不会纳入校验。在缓存的查找过程中,会将这些校验和和已存在镜像中的文件校验和进行对比。如果文件有任何改变,比如内容和元数据,则缓存失效。
  • 除了 ADD 和 COPY 指令,缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配。例如,当执行完 RUN apt-get -y update 指令后,容器中一些文件被更新,但 Docker 不会检查这些文件。这种情况下,只有指令字符串本身被用来匹配缓存。

一旦缓存失效,所有后续的 Dockerfile 指令都将产生新的镜像,缓存不会被使用。

正确使用ADD和COPY指令

这两者很相似,推荐有限选择 COPY,它比 ADD 透明度更高。

  • COPY,只支持将本地文件复制到容器中
  • ADD,除了 COPY 的功能外,还支持远程 URL。但最好的用途是将本地 tar 文件提取到镜像中 ADD rootfs.tar.xz /。

如果在 Dockerfile 中使用不用的文件,那么 COPY 它们可以单独使用。这样,特定文件的更改,将确保每一步的构建缓存无效, 如:

代码语言:javascript
复制
DOCKERFILECOPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

将 COPY . /tmp/ 放在后面,这能够使 RUN 的缓存无效的数量减少。尽量使用docker volume共享文件,而不是用ADD指令添加文件;

不要在Dockerfile中单独修改文件权限

因为 docker 镜像是分层的,任何修改都会新增一个层,修改文件或者目录权限也是如此。如果有一个命令单独修改大文件或者目录的权限,会把这些文件复制一份,这样很容易导致镜像很大;

解决方案也很简单,要么在添加到 Dockerfile 之前就把文件的权限和用户设置好,要么在容器启动脚本(entrypoint)做这些修改,或者拷贝文件和修改权限放在一起做(这样最终也只是增加一层;

版本控制和自动构建

最好把 Dockerfile 和对应的应用代码一起放到版本控制中,然后能够自动构建镜像。这样的好处是可以追踪各个版本镜像的内容,方便了解不同镜像有什么区别,对于调试和回滚都有好处。

另外,如果运行镜像的参数或者环境变量很多,也要有对应的文档给予说明,并且文档要随着 Dockerfile 变化而更新,这样任何人都能参考着文档很容易地使用镜像,而不是下载了镜像不知道怎么用。

RUN指令

为了使Dockerfile易读、易理解和可维护,在使用比较长的RUN指令是可以使用反斜杠\分隔多行。将多行参数按字母顺序排序(比如要安装多个包时)。这可以帮助你避免重复包含同一个包,更新包列表时也更容易。也便于 PRs 阅读和审查。建议在反斜杠符号 \ 之前添加一个空格,以增加可读性。

代码语言:javascript
复制
RUN yum update && yum install -y \
  vim \
  ntpdate \
  git \
  nginx

CMD和ENTRYPOINT指令

CMD和ENTRYPOINT指令指定了容器运行的默认命令,推荐二者结合使用。使用exec格式ENTRYPOINT指令设置固定的默认命令和参数,然后使用CMD指令设置可变的参数。

不要在Dockerfile中做端口映射

Docker的两个核心概念是可重复性和可移植性,镜像应该可以在任何主机上运行多次。映射端口会破坏镜像的可移植性,且这样的镜像只能在一台主机上启动一个容器。所以端口映射应在docker run命令中用-p参数指定。

代码语言:javascript
复制
# 不要在Dockerfile中做如下映射
EXPOSE 80:8080
# 仅仅暴露80端口,需要另做映射
EXPOSE 80

使用多阶段构建

在 Docker 17.05 以上版本中,你可以使用 多阶段构建 来减少所构建镜像的大小;

避免安装不必要的包

为了降低复杂性、减少依赖、减小文件大小、节约构建时间,你应该避免安装任何不必要的包。例如,不要在数据库镜像中包含一个文本编辑器。

一个容器只运行一个进程

应该保证在一个容器中只运行一个进程。将多个应用解耦到不同容器中,保证了容器的横向扩展和复用。例如 web 应用应该包含三个容器: web应用.数据库,缓存;

如果容器互相依赖,你可以使用 Docker 自定义网络 来把这些容器连接起来。

镜像层数尽可能少

你需要在 Dockerfile 可读性(也包括长期的可维护性)和减少层数之间做一个平衡;

用python -m pip而不是pip

这是为了确保我们使用的 pip 是我们想用的那个 python 对应的 pip。有时候,一个系统里安装了 Python 2 和 Python 3,而我们可能错误地设置了 PATH 环境变量(或则因为其他的原因),导致我们运行 python 命令的时候,启动的 Python 3(或者 2),但是 pip 命令是 Python 2(或者3)的 pip。还有一些其他原因使我们更应该用 python -m pip 的,详见 https://snarky.ca/why-you-should-use-python-m-pip/

一个典型的例子(升级 pip)

代码语言:javascript
复制
python -m pip install --quiet --upgrade pip

让pip install 更安静

上例中,在 pip install 命令里,我们用了 --quiet 参数,减少 pip install 打印出来的信息。这样可以让 docker build 更安静。尤其是,如果在 CI 里运行 docker build 的话,减少打印信息可以让 CI log 更加可读。

让apt-get install 更安静

类似的,用 apt-get 安装软件包的时候,我们用 -qq 命令,甚至重定向输出到 /dev/null 让它更安静。

代码语言:javascript
复制
apt-get -qq update
apt-get -qq install -y curl > /dev/null

让curl和wget更安静

首先,如果要下载文件,curl 和 wget 二选一即可。如果用 curl,可以用 --silent 参数

代码语言:javascript
复制
curl -sLO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64

wget 有 --quiet 参数

代码语言:javascript
复制
wget -q https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64

用axel而不是curl或wget

作为一个开源软件,中外开发者都会运行 docker build。开发者所处的地理位置不同,各自都希望从距离自己最近(最快)的 mirror 下载和安装文件。axel 可以从多个 mirror 下载同一个文件,根据各个 mirror 的速度,决定分别从不同 mirror 下载的字节数量。如果有的 mirror 挂了,axel 可以忽略之。尤其对于身处国内的开发者,axel 完全可以取代 curl 以及 wget;

axel 和 wget 一样支持 --quiet 参数。以下是一个从大洋两岸的 mirrors 下载 Go 编译器的例子;

代码语言:javascript
复制
echo "Install Go compiler ..."
GO_MIRROR_0="http://mirrors.ustc.edu.cn/golang/go1.13.4.linux-amd64.tar.gz"
GO_MIRROR_1="https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz"
axel --quiet --output go.tar.gz $GO_MIRROR_0 $GO_MIRROR_1`````````````````````

让python setup.py更安静

有时候我们会在 Dockerfile 里 build 和 install Python packages,此时我们需要运行

代码语言:javascript
复制
python ./setup.py build --quiet
python ./setup.py install --quiet

不过如果我们要 build binary distribution package,则需要注意使用全局参数 --quiet

代码语言:javascript
复制
python ./setup.py --quiet bdist_wheel

明辨ARG和ENV

ARG 和 ENV 是 Dockerfile 里用来定制化 Docker image 的利器,经常结合在一起使用,也常领 Dockerfile 新手挠头。其实,记住一下几条规则,基本就可以了;

  1. ARG 存在于 docker build 命令执行期间。默认值写在 Dockerfile 里。如果需要修改,可以通过 docker build 命令里的 --build-arg 参数来指定。
  2. ENV 存在于 docker run 命令执行期间。默认值写在 Dockerfile 里。如果要修改,可以通过 docker run 命令的 --env 参数来指定。
  3. 如果要把 ARG 的值保存到 container 运行起来之后仍然可以可用,则需要在 ARG 之后写一个 ENV。

为了方便理解,请看下面几个例子。第一个例子:为了把 ARG 的值保存到 docker run 的时候也可以被用到,我们把它写入一个文件 /root/hello.sh;

代码语言:javascript
复制
FROM ubuntu:18.04
ARG releaser=youmen
RUN echo "echo $releaser" > /root/hello.sh
RUN chmod +x /root/hello.sh

这样,我们可以 docker run 的时候运行 /root/hello.sh,打印出 docker bulid 的时候指定的 releaser;

代码语言:javascript
复制
docker build -t dev .
docker run --rm -it dev bash -c /root/hello.sh # 打印出 youmen

不过因为 ARG 只存在于 docker build 命令执行期间,所以下面命令什么也打印不出来

代码语言:javascript
复制
docker run --rm -it dev bash -c "echo $releaser"

如果要让上面命令也可以打印出 releaser 这个 ARG 的值,可以在 Dockerfile 里加一个 ENV;

代码语言:javascript
复制
FROM ubuntu:18.04
ARG releaser=王益
ENV releaser=$releaser

这样,下面命令就也可以打印出”王益“了;

代码语言:javascript
复制
docker build -t dev .
docker run --rm -it dev bash -c "echo $releaser"

docker build --quiet

上面一些经验是让 docker build 变得更安静的。如果要极端的安静,不需要通过在写 Dockerfile 的时候注意什么,只需要在 docker build 命令里加上 --quiet

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用标签
  • 使用统一的Base镜像
  • 充分利用缓存
  • 正确使用ADD和COPY指令
  • 不要在Dockerfile中单独修改文件权限
  • 版本控制和自动构建
  • RUN指令
  • CMD和ENTRYPOINT指令
  • 不要在Dockerfile中做端口映射
  • 使用多阶段构建
  • 避免安装不必要的包
  • 一个容器只运行一个进程
  • 镜像层数尽可能少
  • 用python -m pip而不是pip
  • 让pip install 更安静
  • 让apt-get install 更安静
  • 让curl和wget更安静
  • 用axel而不是curl或wget
  • 让python setup.py更安静
  • 明辨ARG和ENV
  • docker build --quiet
相关产品与服务
容器镜像服务
容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档