2.1.1、Dockerfile最佳实战

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部分

一般,为让你的 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

更多的信息请移步《Dockerfile参考》的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

更多信息请移步《Dockerfile参考》的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很有参考价值:

其他资源:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值