Dockerfile最佳实践【原创、很多实践经验】

首先,参见官方文档:

dockerfile_best-practices

有如下几点说明:红色标注的是重点


怎么理解“合理利用缓存”?

    即:尽量把变化频率小的往前放,经常可能变化的命令往后放。

    因为假设把经常变化的指令放在前面,缓存没有命中,则后面都要重新打镜像。

    类似 COPY WORKDIR ENV LABEL等命令,可以往后放。


什么是“多阶段构建”?

    多阶段构建的应用场景:

    需要在容器中build应用,生成目标文件,然后再把目标文件拷贝到容器中运行。

    比如:

  1. 编写编译容器Dockerfile,把源Java代码拷贝进容器,然后编译,生成在一个目录中。

    docker build -t java:build .

  2. 运行java:build容器,然后把编译好的产物从容器拷贝出来到宿主机上

    docker cp 容器:/home/java/code.class  ./

  3. 编写生产环境Dockerfile

  4. 将code.class拷贝到生产环境Dockerfile中,最终生成目标镜像

    docker build java:production .

    rm  code.class #删除宿主机的文件

    docker rmi java:build  #删除无用(中间状态的构建容器)

    docker rm -f java:build容器 

    使用“多阶段构建”,将编译过程和打包过程,合并在一起,build过程中,会自动运行中间状态容器中的命令,拿到中间产物。Dockerfile如下:

FROM java as build #1.构建阶段别名
COPY code.java /home/java
WORKDDIR  /home/java
RUN java -c code.java

FROM java as production #2.构建阶段别名
#重点!!! 直接从第一阶段拷贝产物文件
COPY --form=build /home/java/code.class /home/java/code.class
WORKDIR  /home/java
CMD ["java","code"]

    直接生成最后一个阶段构建的容器

docker build -t java:production .

    假设想单独生成某个阶段容器

docker build -t java:build  --target=build(构建阶段名称)  .

    注意,这个中间件状态容器,并没有CMD和ENTRYPOINT,没有真正的启动起来,但是build过程中RUN指令会执行。要注意一点的是,RUN指令不要长期运行,应该是运行一段时间就能结束的。


Dockerfile指令优化


1、COPY指令和ADD指令的区别

1)COPY 是传统的复制文件

    格式:COPY <源路径>... <目标路径>

    <目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。

    目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

    此外,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。


2)ADD 是更高级的复制文件指令,在 COPY 基础上增加了一些功能。

    比如 <源路径> 可以是一个 URL,或者压缩文件,ADD会自动拉取、解压文件。

    在 Docker 官方的最佳实践文档中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。


    因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。


2、CMD 与 ENTRYPOINT的区别

CMD

    CMD 指令设置镜像中的默认启动命令和参数. 容器启动时,如果没有单独指定启动命令,则默认执行镜像中 CMD。

    设置启动命令时, 应该尽量使用 JSON 格式 CMD ["command", "arg1", "arg2"]

    例如 nginx 的启动方式: CMD ["nginx", "-D"]


ENTRYPOINT

    当启动主程序之前还需要执行大量的前置操作时, 可以将 ENTRYPOINT 的入口指令设置为一个脚本 start.sh。

    当 dockerfile 中指定了 ENTRYPOINT 的时候, docker run 如果在镜像之后添加的指令, 那么这些指令将被当做 ENTRYPOINT 的参数执行。

    如果 dockerfile 中同时有 CMD 和 ENTRYPOINT 指令, 当 CMD 指令可执行时, 它将在 ENTRYPOINT 之前运行; 如果 CMD 不是可执行的命令, 则将作为 ENTRYPOINT 的命令参数追加。


下面这种写法,执行时,相当于 top -b -c

FROM ubuntu
ENTRYPOINT [ "top", "-b" ]
CMD [ "-c" ]

CMD和ENTRIPOINT详解:https://www.jb51.net/article/136264.htm


注意:

1、CMD命令会被docker run命令覆盖,但是ENTRYPOINT不会,但也可以使用--entrypoint参数指定新的脚本。

2、CMD [xxx] 命令不能用 && 串联多个指令。

比如 CMD ["ls", "&&", "tail", "-f", "app.log"],这样写是错的,只能像下面这样写:

CMD ls && tail -f app.log

3、但是CMD后面跟的命令,有些限制,比如 nohup start.sh & 加上 tail -f app.log

CMD nohup start.sh & && tail -f app.log

这样写是错的。建议改成sh脚本。


ENTRIPOINT编写指南:

1) set -e

    你写的每个脚本都应该在文件开头加上set -e, 这句语句告诉bash如果任何语句的执行结果不是true则应该退出

2) exec "$@"

    几乎在每个docker-entrypoint.sh脚本的最后一行, 执行的都是 exec "$@"命令,它的意思是匹配所有参数,原封不动的执行。

    这个命令的意义在于你已经为你的镜像预想到了应该有的调用情况, 当实际使用镜像的人执行了你没有预料到的可执行命令时, 将会走到脚本的这最后一行, 去执行用户新的可执行命令。

    参见:https://www.cnblogs.com/breezey/p/8812197.html

3) entry-point指令的正常执行的最后一句,要前台执行

    为什么?因为“docker容器在其主进程完成时退出”,entry-point执行完后,docker容器就退出了。让docker容器一直运行的办法就是,entry-point命令不结束,如果最后一个命令是后台运行,entry-point脚本就结束了。

    所以,建议使用 exec 执行最后一个命令。如果最后一句是执行一个脚本,那么这个脚本中的最后一个命令也要后台运行。举个例子entry-point最后一句是 exec start.sh,start.sh里面最后一句是exec run.sh,那么实际上最后执行的是run.sh,务必保证它是前台运行(执行完不会退出的)。

    如果最后那个命令不支持前台运行。那么下面是两种保持脚本不退出的解决方案:

if [[ $1 == "-d" ]]; then    
    while true; do sleep 1000; done    
fi    

if [[ $1 == "-bash" ]]; then    
    /bin/bash    
fi    


3、WORKDIR

    尽量使用绝对路径;

    切换目录的时候尽量使用 WORKDIR, 而不是使用 RUN cd /data。


4、USER

    如果容器中的应用程序运行时不需要特殊的权限, 可以通过 USER 指令把应用程序的所有者设置为非 root 用户. 如果该用户不存在, 首先需要使用 RUN 命令在镜像中创建用户。

    如果在每次编译镜像时, 对用户的 UID/GID 有要求需要保持一致, 应该在新建用户和组的时候指定 UID和 GID。

    在镜像中避免使用sudo 命令,因为该命令使用的 TTY 不确定, 对接收信号量也会造成影响。如果确实需要使用 sudo 功能, 则可使用 gosu 命令替代。

    可以用 root 用户初始化一个 daemon, 然后用非 root 用户启动这个 daemon

    为了减少镜像体积, 应该避免不必要的用户切换。


5、Label

添加Label,以帮助按项目组织镜像,记录许可信息,帮助自动化或其他原因。

Label会增加层数,例如下面的:

# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""

旧版本docker建议将多行Label合并成一行,但是新版本(1.10以后),多行LABEL只会增加1层,不会增加多层。


6、Env

    Each ENV line creates a new intermediate layer, just like RUN commands. This means that even if you unset the environment variable in a future layer, it still persists in this layer and its value can be dumped. You can test this by creating a Dockerfile like the following, and then building it.

FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
$ docker run --rm test sh -c 'echo $ADMIN_USER'
mark

    To prevent this, and really unset the environment variable, use a RUN command with shell commands, to set, use, and unset the variable all in a single layer. You can separate your commands with ; or &&. If you use the second method, and one of the commands fails, the docker build also fails.

FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh

    总结:如果是固化到镜像中的环境变量,使用ENV XXX=xxx,但如果只是临时使用,则使用 RUN export XXX=xxx格式。


7、RUN

    apt-get指令的话,推荐使用固定格式的“缓存清除”指令,形如:

RUN apt-get update && apt-get install -y

    Docker官方的示例为:

RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*

    注意,官方 Debian and Ubuntu images 会自动 run apt-get clean, 因此不需要显示声明。

    另外,注意bash的管道( | )问题:

RUN wget -O - https://some.site | wc -l > /number

    Docker executes these commands using the /bin/sh -c interpreter, which only evaluates the exit code of the last operation in the pipe to determine success. 上例中只要 wc -l 命令成功,build就成功了,即使 wget 命令失败了。

    If you want the command to fail due to an error at any stage in the pipe, 设置 set -o pipefail && to ensure that an unexpected error prevents the build from inadvertently succeeding. For example:

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

    注意:不是所有的 shells 都支持 -o pipefail 选项:

    比如 the dash shell on Debian-based images, 建议使用下面的格式来显式声明/bin/bash 设置 pipefail 选项:

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site |


8、使用最小化Linux镜像时,添加必要常用指令

    例如ping、wget、curl、tar、tail、more、vi、vim、cat、sed、netstat、ps、top、ifconfig、hostname、telnet、lsof、tcpdump、tree、tee、cut、wc、touch、find、head、sort、du、df、ip、nslookup、route、traceroute等。

    实际经验:如果没有这些基础命令,需要在容器内排查问题,比如查看端口、tcp连接、线程等,都做不到。


9、Dockfile离线安装和使用本地文件、软件包时不使用ADD和COPY

    举个例子,你在Dockerfile中,要安装一个 xxxx.rpm 包,这个包有5GB,如果使用COPY或者ADD指令,将添加一个包含了这个5GB的文件层,简单的解决办法为搭建一个HTTP Server,通过url去下载这个本地文件。

    python3以下,在文件目录执行:

python -m SimpleHTTPServer 8069

    python3执行:

python -m http.server --bind 192.168.178.20 8000

然后就可以使用 wget http://192.168.178.20:8000/xxxx.rpm 下载这个文件了。


实践经验

参见:《我的Dockerfile构建笔记》


© 2009-2020 Zollty.com 版权所有。渝ICP备20008982号