首先,参见官方文档:
有如下几点说明:红色标注的是重点
Create ephemeral containers(构建无状态的容器)
Understand build context(理解上下文,不引入多余文件)
Pipe Dockerfile through stdin(无需上下文的情况,通过stdin构建)
Exclude with .dockerignore(排除context中文件,参见 .dockerignore文件)
Use multi-stage builds(多阶段构建,并合理利用缓存:排序原则->最基础的RUN放在前面)
Don’t install unnecessary packages(不安装不必要的包,中间过程文件,可以删除和clean:rm -rf src/* && yum clean all)
Decouple applications(为了更好管理容器,不推荐在一个容器中部署多个进程)
Minimize the number of layers(减少层数,合并RUN、COPY、ADD、LABEL)
Sort multi-line arguments(为了直观,RUN参数较多时,建议分成多行并排序)
Leverage build cache(同上,多阶段构建合理利用缓存)
Dockerfile instructions(Dockerfile指令优化)
怎么理解“合理利用缓存”?
即:尽量把变化频率小的往前放,经常可能变化的命令往后放。
因为假设把经常变化的指令放在前面,缓存没有命中,则后面都要重新打镜像。
类似 COPY WORKDIR ENV LABEL等命令,可以往后放。
什么是“多阶段构建”?
多阶段构建的应用场景:
需要在容器中build应用,生成目标文件,然后再把目标文件拷贝到容器中运行。
比如:
编写编译容器Dockerfile,把源Java代码拷贝进容器,然后编译,生成在一个目录中。
docker build -t java:build .
运行java:build容器,然后把编译好的产物从容器拷贝出来到宿主机上
docker cp 容器:/home/java/code.class ./
编写生产环境Dockerfile
将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 下载这个文件了。