Dockerfile最佳实战
概述
Docker 可以解析Dockerfile
文件中的指令自动构建镜像,这个文件包含了构建指定镜像的所需的所有指令。Dockerfile
遵循特定的格式,使用特定的指令集。你可以在《Dockerfile 参考》页面学习到更多基础知识。如果你是一个编写Dockerfile
的新手,你应该从这里开始。
这份文档覆盖了Docker最佳规范和推荐的方法,使用这些方法可以编写出易用、高效的Dockerfile
。我们强烈地建议你遵循下面的推荐(其实,如果你正在创建一个官方的镜像,你必须遵循这些规范)。
在buildpack-deps Dockerfile
里,你可以看到许多这样的规范和建议。
注意:想获得在这里提及的任何Dockerfile 命令的更多详细说明,请移步到《Docker 参考》页面。
通用指南和建议
容器应该是“朝生暮死”
用于创建容器的镜像,你的 Dockerfile
定义应该尽可能的小。这里的“朝生暮死”,意思是它在停止、摧毁、新建和实施各个阶段,只需一个绝对小的配置和设置。
使用一个.dockerignore文件
在大部分情况下,最好的做法是将每一个Dockerfile文件放到一个空的文件夹里。接着,把构建Dockerfile所需的文件添加到这个文件下。为了提高构建的效率,你可以在这个文夹下添加一个.dockerignore
文件来排除那些没用的文件和文件夹。这个文件支持类似 .gitignore
文件那样的排除模式。关于如何创建它,可以移步到dockerignore 文件。
避免安装不需要的包
为了减少复杂性、依赖性,文件大小和构建的时间,你应该避免安装额外的或不需要的包。例如,你不需要在一个数据库镜里包含一个文本编辑器。
一个服务一个容器
几乎在所有的情况下,你都应该遵循一个服务一个容器的规范。解耦应用程序到多个容器中,是它更易横向扩展和重用容器。如果这个服务依赖另外服务,可以使用容器互联。
层数最小化
你需要在 Dockerfile
文件的可读性(处于长期维护考虑)与它使用的最小层数之间做折中的取舍。关于层数,你应持有谨慎的态度。
分多行编排你的参数
无论什么时候,都应该尽可能根据字母顺序多行的编排命令行参数。这将帮助你避免包的重复和易于更新。这也让PRs的阅读和检查。在反斜杠(\
) 钱加一个空格也很有帮助。
这里有一个来自 buildpack-deps
镜像的例子:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
构建缓存
在构建一个镜像期间,Docker将会更具指定的顺序逐步执行 Dockerfile
文件中的命令。随着每条指令的执行,Docker将会到它的缓存中去寻找可以重用的镜像,而不是再去创建一个重复的镜像。如果你完全不想使用缓存,你可以在 docker build
命令里使用 --no-cache=true
选项去关闭它。
然而,如果你想让Docker使用它的缓存,那么,理解它什么时候去寻找一个镜像是很重要的。Docker将遵循下面的这些规则:
-
从缓存中的基础镜像开始,往下的每条命令将与构建子镜像的指令相比较,寻找与之相同的指令,如果没有找到,缓存不起作用。
-
在大多数情况下,简单比较
Dockerfile
里面的指令和子镜像的构建指令,已经可以满足要求,然而,某些指令还是需要更多的检查和说明。 -
ADD
和COPY
指令,会检查镜像中的每一个文件的内容校验和。文件的最后修改和最后访问时间不含在这个校验和中。在查找缓存期间,与已存在的镜像校验和相比较。如果这些文件发生任何改变,诸如文件的内容、元数据,那么这个缓存就不起作用。 -
除了
ADD
和COPY
命令外,缓存检查不会查看容器里的文件而决定是否匹配。例如,当处理RUN apt-get -y update
命令时,更新容器里的文件前,不再去检查缓存中是否已经存在这个文件。这个时候仅仅对命令行本身做查找匹配而已。
一旦缓存时效时,所有后续的 Dockerfile
命令将生成新的镜像,而不再使用缓存。
Dockerfile 指令
下面,你将获得一些建议,关于如何更好的使用不同的指令,在编写一个 Dockerfile
文件时。
FROM
更多信息请移步《Dockerfile 参考》的FROM部分
无论什么时候,尽可能使用最新的官方仓库作为你的的镜像基础。我们推荐 Debian的镜像,虽然它被设计的非常紧凑简小(目前不到100兆),但是仍然是一个完整的发行版本。
RUN
一般,为让你的 Dockerfile
更加易读,易懂和便于维护,请将长的或者复杂的 RUN
语句用反斜杠(\)分割成多行。
RUN
一般都是搭配 apt-get
一起使用。当使用 apt-get
时,这里几个注意事项:
-
不要在单独一行上使用
RUN apt-get update
。 这样会引起缓存问题,如果关联的归档文档被更新了,将会导致后续的apt-get install
执行失败而没有任何提示。 -
避免
RUN apt-get upgrade
或dist-upgrade
, 因为很多来自基础镜像的“底层”的包将会更新失败,在一个无特权的容器里。如果一个基础包已经过期,你应该通知它的维护人员。如果你知道这里一个特定的包,如foo
,它需要更新,可以直接使用apt-get install -y foo
让它自动更新。 -
应该这样编写你的指令:
RUN apt-get update && apt-get install -y \ package-bar \ package-baz \ package-foo
使用这样方法编写指令,不仅让它变得更加易读和可维护,而且,通过包含 apt-get update
,确保绕开本地的缓存,安装最新的版本而不需要编写更多的指令和手动的干预。
- 绕开缓存可以实现包的版本定位(例如:
package-foo=1.3.*
)。这将强制去检索指定的版本,不管缓存里存储了什么。编写你的apt-get
代码,这种方法将大大降低的维护难度和减少由未意料的的包而导致失败概率。
例子
下面是一段格式良好的 RUN
指令,它演示了上述的建议。注意最后的包 s3cmd
,指定了一个版本 1.1.0*
。如果这个镜像之前使用过一个就得版本,指定的新版将引起 apt-get update
缓存失效,确保一个新的版本被安装(在这个应用场景中,需要这个特性)。
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
btrfs-tools \
build-essential \
curl \
dpkg-sig \
git \
iptables \
libapparmor-dev \
libcap-dev \
libsqlite3-dev \
lxc=1.0* \
mercurial \
parallel \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.0*
使用这种方法编写指令也可以帮助你避免包的重复,因为这样写比下面的写法更加的易读:
RUN apt-get install -y package-foo && apt-get install -y package-bar
CMD
CMD
命令应该用来运行包含软件的镜像,连同任何参数。CMD
应该总是使用这种格式CMD [“executable”, “param1”, “param2”…]
。 这样,如果这个镜像承载着一个服务(Apache,Rails等),你可以运行类似CMD ["apache2","-DFOREGROUND"]
的指令。 事实上,这种格式的指令,无论那种基于服务的镜像,都值得推荐。
在大多的其他场景里,CMD
应该指定一个交互式的shell (bash, python, perl, 等),例如,CMD ["perl", "-de0"]
, CMD ["python"]
, 或 CMD [“php”, “-a”]
。 使用这些格式类似你执行docker run -it python
,你将进入一个可用的shell中,准备好了。当CMD
和ENTRYPOINT
协同工作时,应该使用 CMD [“param”, “param”]
格式。这种方式尽量少用,除非你和你的用户对 ENTRYPOINT
实现机制都很了解。
EXPOSE
更多的西悉尼请移步《Dockerfile参考》的EXPOSE部分
EXPOSE
指令指定容器监听的端口。因此,你应该使用通用、惯例的端口到你的应用。例如,一个包含着Apacheweb服务端的镜像将使用80端口,当镜像包含是一个MangoDB应该使用EXPOSE 27017
等等。
为了提供外部访问,你的用户可以执行docker run
带上一个标志,表明如何映射指定的端口到他们选择的端口。为了容器的连接,Docker提供了环境变量来指定接受容器到源容器的路径(如,MYSQL_PORT_3306_TCP
)。
ENV
更多的信息请移步《Dockerfile参考》的ENV的部分
为了方便新安装的软件的运行,你可以使用ENV
去更新环境变量PATH
。例如,ENV PATH /usr/local/nginx/bin:$PATH
保证CMD [“nginx”]
可以正常运行。
ENV
指令也可以为容器化的运用提供必需的环境变量,比如,Postgres的 PGDATA
。
最后,ENV
也可以用来设置常用的版本号,这样,可以让版本维护更加容易,正如下面的例子:
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
和在编程时定义常亮类似(而不采用硬编码),使用这样方法,你只需修改一个ENV
指令,就能自动更新与之关联的数据。
ADD 或 COPY
更多信息请移步《Dockerfile参考》的 ADD部分
更多信息请移步《Dockerfile参考》的 COPY部分
虽然 ADD
和COPY
的功能类似,一般而言,推荐使用COPY
。因为它比ADD
更加见名知意。COPY
只支持将本地本件拷贝到容器中,虽然ADD
拥有一些功能(例如,抽取本地tar文件内容和支持远程URL),但是这些功能不是很常用。因此,ADD
的最佳使用场景是,自动抽取一个本地tar的内容到镜像中,例如:ADD rootfs.tar.xz /
。
如果你要执行多个Dockerfile
步骤且使用来自的环境中不同的文件,分开COPY
它们,而不是一次性的拷贝它们。这样可以确保每个步骤的构建缓存都是失效的(强制步骤的重做),如果指定需要的的文件更新了。
例如:
COPY requirements.txt /tmp/
RUN pip install /tmp/requirements.txt
COPY . /tmp/
这样,RUN
步骤可以增加缓存的命中率,如果你把COPY . /tmp/
放到它前面,反之。
处于镜像的大小的考虑,使用 ADD
从远程URL提出t内容的方法强烈不推荐。你应该使用curl
或 wget
替代。这种方法允许你在提取完内容后,可以删除你不需要的文件。例如,你应该避免这样做:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all
相反,你应该这样做:
RUN mkdir -p /usr/src/things \
&& curl -SL http://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
除了需要从tar文件中提取内容时使用ADD
,其他时候,你应该总是使用COPY
。
ENTRYPOINT
更多信息请移步《Dockerfile参考》的 ENTRYPOINT部分
ENTRYPOINT
最佳使用场景是设置镜像的主入口命令,允许镜像好像命令运行一样(使用 CMD
作为默认的标志)。
让我们启动一个带命令行工具 s3cmd
的镜像:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
现在,启动后的镜像与在命令行中执行命令的帮助类似:
$ docker run s3cmd
或在右边添加参数来执行一个命令:
$ docker run s3cmd ls s3://mybucket
这很用,如上所述,可以把镜像的名字当做一个二进制程序来使用。
ENTRYPOINT
指令也可以和一个辅助脚本结合使用,允许它和上述的类似方式运行,即使当启动工具命令超过一行时。
例如,Postgres官方镜像使用下面的脚本作为它的ENTRYPOINT
:
#!/bin/bashset -eif [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fiexec gosu postgres "$@"fiexec "$@"
注意:这个脚本使用了
exec
Bash指令,运行时的应用程序会变成容器的PID 1。这将允许应用可以接收发送到容器的所有Unix信号。 查看ENTRYPOINT
帮助文档获得更多的信息。
将这个辅助脚本拷贝到容器里,通过 ENTRYPOINT
来启动容器:
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
这个脚本允许用户使用几种交互的方法启动Postgres:
可以简单的启动Postgres:
$ docker run postgres
或者,可以使用它去运行带几个参数的Postgres:
$ docker run postgres postgres --help
最后,它也可以用来启动一个完全不同的工具,如,Bash:
$ docker run --rm -it postgres bash
VOLUME
更多信息请移步《Dockerfile参考》的VOLUME部分
VOLUME
指令应该用于暴露任何的数据库存储域、配置存储、文件/文件夹,在创建容器的时候。任何易变的或镜像的供用户使用的部分,建议使用VOLUME
。
USER
如果一个服务可以不需要权限就能运行,应该使用 USER
切换到一个非root用户。使用像这种命令 RUN groupadd -r postgres && useradd -r -g postgres postgres
可以创建一个用户和用户组。
注意:镜像里的用户和组的UID/GID都是不确定的,不管它是否被重建。如果这些信息对你很重要,你应该显示的指定一个UID/GID。
你应该避免安装或使用 sudo
,因为这些操作带来不确定的TTY和信号的转发行为,是一个得不偿失的设置。如果你必需要使用类似 sudo
的功能(例如,在非root用户在初始化一个需要root权限的的守护进程),你可能需要使用“gosu”。
最后,为了减少层和复杂度,不建议频繁的来回切换 USER
。
WORKDIR
更多内容请移步《Dockerfile参考》的WORKDIR部分
为了清晰和可靠,你应该始终为你的WORKDIR
指定一个绝度路径。另外,你因该使用WORKDIR
来替代类似RUN cd … && do-something
指令,这样可以降低可读性、故障排除难度、维护成本。
ONBUILD
更多内容请移步《Dockerfile参考》的ONBUILD部分
一个ONBUILD
命令在当前的Dockerfile
构建完成后会被执行。当使用 FROM
为镜像个派生出子镜像时,ONBUILD
也会被执行。也可以简单的理解为,其实是将父Dockerfile
的ONBUILD
中的指令放到子Dockerfile
中。
ONBUILD
命令会先于子Dockerfile
中所有命令执行。
ONBUILD
对使用 FROM
基于指定镜像构建很有帮助。例如, ONBUILD
允许你在 Dockerfile
里,基于某种语言栈构建任意的软件镜像,你可以参考Ruby的 ONBUILD
。.
ONBUILD
因该指定一个指定标志(tag),例如:ruby:1.9-onbuild
或 ruby:2.0-onbuild
。
当你把 ADD
或 COPY
放到ONBUILD
要注意。 如果新的构建环境缺少要添加的资源,会导致镜像的构建失败。添加一个分隔标签,如条建议一样,以供编写的 Dockerfile
可以选择合适他的构建环境。
官方的仓库例子
这些官方的仓库 Dockerfile
很有参考价值: