Dockerfile编写规范
本⽂主要介绍在编写 docker 镜像的时候⼀些需要注意的事项和推荐的做法。
虽然 Dockerfile 简化了镜像构建的过程,并且把这个过程可以进⾏版本控制,但是不正当的
Dockerfile 使⽤也会导致很多问题:
1. docker 镜像太⼤。如果你经常使⽤镜像或者构建镜像,⼀定会遇到那种很⼤的镜像,甚⾄有些能达到 2G 以上
2. docker 镜像的构建时间过长。每个 build 都会耗费很长时间,对于需要经常构建镜像(⽐如单元测试)的地⽅这可能是个⼤问题
3. 重复劳动。多次镜像构建之间⼤部分内容都是完全⼀样⽽且重复的,但是每次都要做⼀遍,浪费时间和资源
希望读者能够对 docker 镜像有⼀定的了解,阅读这篇⽂章⾄少需要⼀下前提知识:
1. 了解 docker 的基础概念,运⾏过容器
2. 熟悉 docker 镜像的基础知识,知道镜像的分层结构
3. 最好是负责过某个 docker 镜像的构建(使⽤ docker build 命令创建过⾃⼰的镜像)
4. Dockerfile 和镜像构建
Dockerfile 是由⼀个个指令组成的,每个指令都对应着最终镜像的⼀层。每⾏的第⼀个单词就是命令,后⾯所有的字符串是这个命令的参数,关于 Dockerfile ⽀持的命令以及它们的⽤法,可以参考官⽅⽂档,这⾥不再赘述。
当运⾏ docker build 命令的时候,整个的构建过程是这样的:
a. 读取 Dockerfile ⽂件发送到 docker daemon
b. 读取当前⽬录的所有⽂件(context),发送到 docker daemon
c. 对 Dockerfile 进⾏解析,处理成命令加上对应参数的结构
d. 按照顺序循环遍历所有的命令,对每个命令调⽤对应的处理函数进⾏处理
e. 每个命令(除了 FROM)都会在⼀个容器执⾏,执⾏的结果会⽣成⼀个新的镜像,为最后⽣成的镜
像打上标签
编写 Dockerfile 的⼀些最佳实践
1.使⽤统⼀的 base 镜像
有些⽂章讲优化镜像会提倡使⽤尽量⼩的基础镜像,⽬前集团操作系统⼀级提供统⼀的基础镜像,⼀些BU也根据⾃⼰的技术规范定义了BU 级的基础镜像,⼀般的应⽤只需要FROM⾃⼰BU提供的基础镜像即可,因为基础镜像只需要下载⼀次可以共享,并不会造成太多的存储空间浪费。它的好处是这些镜像的⽣态⽐较完整,⽅便我们安装软件,除了问题⽅便调试。
2.动静分离
经常变化的内容和基本不会变化的内容要分开,把不怎么变化的内容放在下层,创建出来不同基础镜像供上层使⽤。⽐如可以创建各种语⾔的基础镜像,这些镜像包含了最基本的语⾔库,每个组可以在上⾯继续构建应⽤级别的镜像。
3.最⼩原则:只安装必需的东西
很多⼈构建镜像的时候,都有⼀种冲动——把可能⽤到的东西都打包到镜像中。要遏制这种想法,镜
像中应该只包含必需的东西,任何可以有也可以没有的东西都不要放到⾥⾯。因为镜像的扩展很容易,⽽且运⾏容器的时候也很⽅便地对其进⾏修改。这样可以保证镜像尽可能⼩,构建的时候尽可能快,也保证未来的更快传输、更省⽹络资源。
4.⼀个原则:每个镜像只有⼀个功能
不要在容器⾥运⾏多个不同功能的进程,每个镜像中只安装⼀个应⽤的软件包和⽂件,需要交互的程序通过容器之间的⽹络进⾏交流。这样可以保证模块化,不同的应⽤可以分开维护和升级,也能减⼩单个镜像的⼤⼩。
5.使⽤更少的层
虽然看起来把不同的命令尽量分开来,写在多个命令中容易阅读和理解。但是这样会导致出现太多的镜像层,⽽不好管理和分析镜像,⽽且镜像的层是有限的。尽量把相关的内容放到同⼀个层,使⽤换⾏符进⾏分割,这样可以进⼀步减⼩镜像⼤⼩,并且⽅便查看镜像历史。
6.减少每层的内容
尽管只安装必须的内容,在这个过程中也可能会产⽣额外的内容或者临时⽂件,我们要尽量让每层安装的东西保持最⼩。
⽐如使⽤ --no-install-recommends 参数告诉 apt-get 不要安装推荐的软件包
7.不要在 Dockerfile 中单独修改⽂件的权限
因为 docker 镜像是分层的,任何修改都会新增⼀个层,修改⽂件或者⽬录权限也是如此。如果有⼀个命令单独修改⼤⽂件或者⽬录的权限,会把这些⽂件复制⼀份,这样很容易导致镜像很⼤。
解决⽅案也很简单,要么在添加到 Dockerfile 之前就把⽂件的权限和⽤户设置好,要么在容器启动脚本(entrypoint)做这些修改,或者拷贝⽂件和修改权限放在⼀起做(这样最终也只是增加⼀层)。
8.利⽤ cache 来加快构建速度
如果 Docker 发现某个层已经存在了,它会直接使⽤已经存在的层,⽽不会重新运⾏⼀次。如果你连续运⾏ docker build 多次,会发现第⼆次运⾏很快就结束了。
不过从 1.10 版本开始,Content Addressable Storage 的引⼊导致缓存功能的实效,⽬前引⼊了 --cache-from 参数可以⼿动指定⼀个镜像来使⽤它的缓存。
9.版本控制和⾃动构建
最好把 Dockerfile 和对应的应⽤代码⼀起放到版本控制中,然后能够⾃动构建镜像。这样的好处是可以追踪各个版本镜像的内容,⽅便了解不同镜像有什么区别,对于调试和回滚都有好处。
另外,如果运⾏镜像的参数或者环境变量很多,也要有对应的⽂档给予说明,并且⽂档要随着 Dockerfile 变化⽽更新,这样任何⼈都能参考着⽂档很容易地使⽤镜像,⽽不是下载了镜像不知道怎么⽤。
10.使⽤⼀个.dockerignore⽂件
在⼤部分情况下,最好的做法是将每⼀个Dockerfile⽂件放到⼀个空的⽂件夹⾥。接着,把构建Dockerfile所需的⽂件添加到这个⽂件下。为了提⾼构建的效率,你可以在这个⽂夹下添加⼀个.dockerignore ⽂件来排除那些没⽤的⽂件和⽂件夹。这个⽂件⽀持类似 .gitignore ⽂件那样的排除模式。关于如何创建它,可以移步到dockerignore ⽂件。
Dockerfile 指令介绍:
更多信息请参考《》
FROM :
这个设置基本的镜像,为后续的命令使⽤,所以应该作为Dockerfile的第⼀条指令。
FROM <image>:<tag>
⽆论什么时候,尽可能使⽤BU提供的基础镜像,有利于技术规范化,简化你的Dockerfile。
RUN :
RUN命令会在上⾯FROM指定的镜像⾥执⾏任何命令,然后提交(commit)结果,提交的镜像会在后⾯继续⽤到。格式
shell创建文件并写入内容RUN <command> (the command is run in a shell - `/bin/sh -c`)
⼀般,为让你的 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 yum update && yum 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 yum update && yum 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*;yum clean all
使⽤这种⽅法编写指令也可以帮助你避免包的重复,因为这样写⽐下⾯的写法更加的易读:
RUN yum install -y package-foo && yum install -y package-bar;yum clean all
EXPOSE
EXPOSE 指令指定容器监听的端⼝。因此,你应该使⽤通⽤、惯例的端⼝到你的应⽤。例如,⼀个包
含着Apacheweb服务端的镜像将使⽤80端⼝,当镜像包含是⼀个MangoDB应该使⽤EXPOSE 27017 等等。
为了提供外部访问,你的⽤户可以执⾏docker run 带上⼀个标志,表明如何映射指定的端⼝到他们选择的端⼝。为了容器的连接,Docker提供了环境变量来指定接受容器到源容器的路径(如,MYSQL_PORT_3306_TCP)。
ENV
为了⽅便新安装的软件的运⾏,你可以使⽤ENV 去更新环境变量PATH 。例如,ENV PATH /usr/local/nginx/bin:$PATH 保证CMD [“nginx”]可以正常运⾏。
ADD 或 COPY
虽然 ADD 和COPY 的功能类似,⼀般⽽⾔,推荐使⽤COPY 。因为它⽐ADD更加见名知意。COPY 只⽀持将本地本件拷贝到容器中,虽然ADD 拥有⼀些功能(例如,抽取本地tar⽂件内容和⽀持远程URL),但是这些功能不是很常⽤。因此,ADD 的最佳使⽤场景是,⾃动抽取⼀个本地tar的内容到镜像中,例如:ADD /。
如果你要执⾏多个Dockerfile 步骤且使⽤来⾃的环境中不同的⽂件,分开COPY 它们,⽽不是⼀次性
的拷贝它们。这样可以确保每个步骤的构建缓存都是失效的(强制步骤的重做),如果指定需要的的⽂件更新了。
例如:
/tmp/
RUN pip install /
COPY . /tmp/
这样,RUN 步骤可以增加缓存的命中率,如果你把COPY . /tmp/ 放到它前⾯,反之。
出于镜像的⼤⼩的考虑,使⽤ ADD 从远程URL提取内容的⽅法强烈不推荐。你应该使⽤curl 或 wget 替代。这种⽅法允许你在提取完内容后,可以删除你不需要的⽂件。例如,你应该避免这样做:
COPY指令是以root⾝份执⾏的。但集团pouch在启动应⽤时会将/home/admin的属主置为admin,所以⽤户⼀般不需要额外的指令来处理COPY到/home/admin/⽬录下的⽂件属主权限。
ENTRYPOINT
ENTRYPOINT 最佳使⽤场景是设置镜像的主⼊⼝命令,允许镜像好像命令运⾏⼀样(使⽤ CMD 作为默认的标志)。
让我们启动⼀个带命令⾏⼯具 s3cmd的镜像:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
现在,启动后的镜像与在命令⾏中执⾏命令的帮助类似:
$ docker run s3cmd
或在右边添加参数来执⾏⼀个命令:
$ docker run s3cmd ls s3://mybucket
这很⽤,如上所述,可以把镜像的名字当做⼀个⼆进制程序来使⽤。
ENTRYPOINT 指令也可以和⼀个辅助脚本结合使⽤,允许它和上述的类似⽅式运⾏,即使当启动⼯具命令超过⼀⾏时。
例如,Postgres官⽅镜像使⽤下⾯的脚本作为它的ENTRYPOINT:
#!/bin/bashset -eif [ "PGDATA"if [ -z "PGDATA")" ]; then
gosu postgres initdb
fiexec gosu postgres "@"
注意:这个脚本使⽤了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
创建⼀个挂载点⽤于共享⽬录。
VOLUME 指令应该⽤于暴露任何的数据库存储域、配置存储、⽂件/⽂件夹,在创建容器的时候。任何易变的或镜像的供⽤户使⽤的部分,建议使⽤VOLUME 。
docker run时会将宿主机的⽬录挂载到VOLUME⽬录下,以宿主机某个⽬录(此镜像独享的⽬录)覆盖docker容器中的对应⽬录,使得其中的数据修改在docker重启时仍然能保持;带来另⼀个后果是,如果你在Dockerfile中往VOLUME⽬录中写⼊了数据(即docker build阶段写⼊的数据),在启动容器的时候你会发现它不见了(因为它写到编译机上去了)。
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 指令,这样可以降低可读性、故障排除难度、维护成本。
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 实现机制都很了解。
ONBUILD
更多内容请移步《Dockerfile参考》的ONBUILD部分
⼀个ONBUILD 命令在当前的Dockerfile 构建完成后会被执⾏。当使⽤ FROM 为镜像个派⽣出⼦镜像时,ONBUILD 也会被执⾏。也可以简单的理解为,其实是将⽗Dockerfile 的ONBUILD 中的指令放到⼦Dockerfile中。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论