Dockerfiles 官网 doc 笔记
Docker 通过 Dockerfile 中的指令来构建镜像。
Docker 镜像由很多镜像构成,每一层对应一个 Dockerfile 里面的指令。
一个运行中的容器,由镜像的所有层加上可写层构成,所有的读写都在最上面的可写层。
容器应该是无状态的,销毁、重建应该花费最小的配置。
(build context)构建上下文
1 | docker build -f ~/Dockerfile.hi context-dir |
通过 -f 指定 Dockerfile 的路径
context-dir
即为构建上下文,该文件夹下面所有的内容都会被传递给 Docker daemon,用来构建镜像。构建上下文包含不相关的内容,会影响构建速度、镜像大小。
可通过编写 .dockerignore
文件,并放置在构建上下文的目录下,达到类似于 .gitignore
的效果,将不必要的文件(夹)不发送给 Docker Daemon。
有如下几种从 stdin
构建镜像的方式:
-
表是占位,从stdin
读取
- 无构建上下文(不需要拷贝文件到镜像中)
模板
1 | docker build [OPTIONS] - |
示例
1 | echo -e 'FROM busybox\nRUN echo "hello world"' | docker build - |
或
1 | docker build -<<EOF |
上述两种方式,都不会给 Docker Daemon 发送构建上下文,但是注意不要在 Dockerfile 中使用 COPY
、 ADD
,这样会导致构建失败。
- 传递本地构建上下文
模板
1 | docker build [OPTIONS] -f- PATH |
示例
1 | create a directory to work in |
- 传递远程构建上下文
模板
1 | docker build [OPTIONS] -f- PATH |
示例
1 | docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF |
多阶段构建
使 Dockerfile 在容易维护、阅读的基础上,减少镜像的大小
基础用法
在一个 Dockerfile 中使用多个 FROM,每个 FROM 构建一个镜像,在镜像之间的拷贝操作变得容易。如下:
1 | FROM golang:1.7.3 |
对 --from=0
,按照数组下标从 0 开始,第一个 FROM 构建的镜像为0,依次类推。当然也可以给 FROM 构建的镜像命名:
1 | FROM golang:1.7.3 AS builder |
其实 --from=xxx
中的 xxx 也可以为其它的、不在此 Dockerfile 中产生的镜像
其它用法
- 只构建指定的构建阶段,即某特定
FROM
所代表的镜像。沿用前面的例子,可以只构建builder
阶段的镜像。
1 | docker build --target builder -t alexellis2/href-counter:latest . |
- 后续的
FROM
可以以前面的FROM
构建的镜像做为 base。
1 | FROM alpine:latest as builder |
构建缓存
显式声明不使用缓存: docker build --no-cache=true ...
。不做显式声明,默认可能会利用构建缓存,能成功利用构建缓存的情况:
- 如果从缓存中存在的一个镜像开始构建,那么会将基于此父镜像的所有子镜像的 Dockerfile 拉出来做一个对比,看下一条指令是否相同,如果不相同,缓存失效。
- 对比 Dockerfile 内容是否相同。一些特殊命令,需要更多检测。
- 对
ADD
和COPY
,它们操作的文件对象都会被作为 checksum 的一部分,不一致则缓存失效。 - 除了
ADD
和COPY
,其他命令带来的文件改变,不会被计入 checksum,也就是在匹配镜像时,会忽略此部分的文件的不同。
其他建议写法
不安装多余的包。降低复杂度、依赖性、镜像大小和构建时间。
解耦应用,一个容器只关心一件事情,以达到水平扩展和容器的复用。但是并意味着一个容器一个进程一成不变。
减少镜像的层数。只有
RUN
,COPY
,ADD
三个指令添加层数,其他的指令只会创建临时的层,并不会增加镜像大小。所以,分多个阶段来构建镜像,只拷贝需要的文件到镜像中,能有效减少镜像的大小。为多行参数排序。按字母顺序排序。避免包重复。如下:
1 | RUN apt-get update && apt-get install -y \ |
Dockerfile 指令
FROM
三种方式
1 | FROM [--platform=<platform>] <image> [AS <name>] |
几个要点
FROM
开启一个构建Dockerfile
的第一个必须是FROM
,但是FROM
前面可以有ARG
来声明变量Dockerfile
中可以出现多个FROM
来实现多阶段构建- 可以为构建阶段添加别名,在后续
FROM
和COPY --from=<name|index>
中使用
ARG
的使用样例
1 | ARG CODE_VERSION=latest |
LABEL
注意事项
- 字符串中含有空格,必须转义或使用
""
- 字符串中含有
"
,必须转义 - 用自己的反转域名作为 label 前缀,必须对域名有权限
Docker
保留的前缀:com.docker.*
,io.docker.*
,org.dockerproject.*
- key 应该使用小写字母数字、
.
、-
-
示例
1 | # Set one or more individual labels |
查看 label
1 | docker image inspect |
RUN
两种形式
1 | # shell form |
shell form 将会在 shell 中运行,Linux 默认为 /bin/sh -C
,Windows 默认为 cmd /S /C
,默认 shell 可以通过 SHELL
指令进行指定。
exec form 会被解析成 JSON 数组,因此需要用 "
扩起来。
注: RUN [ "echo", "$HOME" ]
无法读取变量,要用 RUN [ "sh", "-c", "echo $HOME" ]
才行。
在镜像顶层之上,创建新的镜像层,然后运行命令,结束后,持久化为一个只读镜像层。
apt-get
使用 RUN
指令,最多的就是执行 apt-get
之类的代码,来安装依赖。有如下注意点:
- 避免运行
apt-get upgrade
和dist-upgrade
- 使用
apt-get install -y foo
来自动升级一个依赖包 - 将
RUN apt-get update
和apt-get install
包含在同一个RUN
指令中
一个比较推荐的写法
1 | RUN apt-get update && apt-get install -y \ |
Debian 和 Ubuntu 会自动运行 apt-get clean
管道符
1 | RUN wget -O - https://some.site | wc -l > /number |
默认只以最后一个命令 wc
的状态作为整个 RUN
指令的成功或失败,即使 wget
失败了。但可以通过设置 set -o pipefail
避免失败被忽略。如下:
1 | RUN set -o pipefail && wget -O - https://some.site | wc -l > /number |
CMD
CMD 指令和我印象中的不太一样。
三种形式
- CMD [“executable”,”param1”,”param2”] (exec form, 首推)
- CMD [“param1”,”param2”] (作为 ENTRYPOINT 的默认参数, 不推荐使用,除非很了解 ENTRYPOINT)
- CMD command param1 param2 (shell form)
只能有一个 CMD 指令,多个 CMD 指令的情况,最后一个 CMD 指令生效。目的是提供可执行命令或参数(此时必须指定 ENTRYPOINT)。
exec form 不提供变量替换。
shell form 默认使用 /bin/sh -c
执行;当执行命令不需要 shell 时,可以使用 exec shell。
在构建期间:RUN 会执行,并将结果作为镜像层进行提交;CMD 则不会执行。
CMD 的使用示例(以 nginx 为例):
- 错误❌ :
CMD service nginx start
- 正确✅ :
CMD ["nginx", "-g", "daemon off;"]
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。CMD service nginx start
会被理解为CMD [ "sh", "-c", "service nginx start"]
,因此主进程实际上是 sh。那么当service nginx start
命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。
EXPOSE
这个指令印象中是暴露一个容器的端口,但仔细想了想这个指令,对它的作用产生了怀疑。如果这个指令能暴露端口的话,那么和
docker run -p
是什么关系。
EXPOSE 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。这个指令的好处为:
- 方便镜像使用者理解以配置端口映射
- 运行时,使用
docker run -P
为被 EXPOSE 声明过的容器的端口,随机映射一个宿主机端口。
ENV
单纯设置环境变量
格式有两种:
1 | ENV <key> <value> |
在 ENV 后面的指令/运行时,能访问该环境变量。
COPY
从宿主机,拷贝文件到镜像中
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。
权限问题:默认情况下,新拷贝的文件的 UID 和 GID 都是 0,即归属 root 用户。可以通过 --chown USER:GROUP
来修改文件所属(仅适合用于创建 Linux 容器)。
从 STDIN 构建的话,没有构建上下文,不能使用 COPY。
COPY 还可以接受 --from=<name|index>
从镜像中拷贝文件。
ADD
之前没见过
与 COPY 类似,但是会自动将压缩包解压到目标文件夹。如下: ADD rootfs.tar.xz /
。
一般情况用 COPY,需要自动解压压缩包的情景再用 ADD。
ENTRYPOINT
这部分内容之前并没仔细了解过,觉得就是简单的容器的启动命令,填上就行,但是看完这部分的文档后,刷新了认知。
两种启动模式:
- exec form (preferred form):
ENTRYPOINT ["executable", "param1", "param2"]
- shell form:
ENTRYPOINT command param1 param2
PID 1
进程
顾名思义,进程号为 1 的进程。特殊的地方在于,PID 1 进程可以接收发给容器的 UNIX 信号。
PID 1 进程的用处,可以通过下面的 Dockerfile 来展示:
1 | FROM ubuntu |
构建完成后,执行下面的命令,可以看到关闭容器花费了非常长的时间:
当执行 docker stop
时,容器并未完全退出, docker stop
会在超时后,发送 SIGKILL
关闭容器中的其他进程。
如果 top -b
是 PID 1 进程呢?修改一下 Dockerfile 如下:
1 | FROM ubuntu |
可以很明显看出,很顺畅地就完成了 docker stop
exec form
ENTRYPOINT 中的命令默认 PID 为 1,即不会通过某种 shell 来启动(也就无法进行变量替换)。
如果是以一个脚本来启动服务,需要在脚本中使用 exec 或 gosu 来执行。
- 什么是 gosu?
- 什么是 exec?
shell form
- CMD、
docker run
命令行参数都会被忽略掉。 - ENTRYPOINT 会以
/bin/sh -c
的子进程运行,即非 PID 1 进程,无法接收 UNIX 信号。 - 可以通过 exec 达到让 ENTRYPOINT 指定的服务 PID 为 1。
ENTRYPOINT & CMD
两者之间的规定:
- Dockerfile 中至少有一个 ENTRYPOINT 或 CMD。也就说可以有多个,但是最后一个才会生效。
- ENTRYPOINT should be defined when using the container as an executable.Why?
- CMD 可以用作 ENTRYPOINT 的默认参数 或 for executing an ad-hoc command in a container.
- CMD will be overridden when running the container with alternative arguments.What?
两者之间的协同关系:
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry (shell form) | ENTRYPOINT [“exec_entry”, “p1_entry”](exec form) | |
---|---|---|---|
No CMD | error, not allowed | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
CMD [“exec_cmd”, “p1_cmd”] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd |
CMD [“p1_cmd”, “p2_cmd”] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
VOLUME
含义:直接设置镜像的一个挂载点
用途:存放镜像创建的文件、配置、数据库的数据文件。易变、镜像中的用户数据。
方式:
1 | VOLUME ["/data"] |
或
1 | VOLUME /var/log |
USER
含义:指定镜像运行时的用户和用户组(不指定默认为 root)。USER
指令之后的 CMD
、RUN
、ENTRYPOINT
都将以 USER
指定的用户和用户组运行。
方式:
1 | USER <user>[:<group>] |
避免使用 sudo
,它会有一定的问题。确有需要考虑使用 gosu
。
WORKDIR
含义:指定工作目录,建议使用绝对路径(使用相对路径时,会以当前目录中的文件夹为工作目录)。可以根据需要多次指定,避免使用 cd。此指令对后续的 Dockerfile 指令生效,如:RUN, CMD, ENTRYPOINT, COPY, ADD
方式:
1 | WORKDIR /path/to/workdir |
ONBUILD
含义:构建 BaseImage 时,加入 ONBUILD 指令,会在使用(FROM BaseImage)的构建中,执行 ONBUILD 指令后,才执行此构建的构建指令。
方式:
1 | ONBUILD ADD . /app/src |
使用场景:使用 maven 编译 jar 包,并构建我们自己的业务镜像。所以我们的 Dockerfile 可以如下编写:
1 | FROM maven:3.3-jdk-8-onbuild |
为啥可以这样?这是因为在 maven:3.3-jdk-8-onbuild 这个镜像中,有 ONBUILD 指令,如下:
1 | FROM maven:3-jdk-8 |
工作流程:
- 遇到 ONBUILD 指令,添加触发器到镜像的 metadata 中,所以 ONBUILD 不影响当前构建的镜像。
- 在构建完成后,触发器会被添加到镜像的 manifest 中,可以通过
docker inspect
进行查看。 - 当被用作基础镜像(FROM xxx)后,新构建执行触发器,即 ONBUILD 指令,全部成功执行完后,才开始新镜像的构建。
- 触发器在执行后会被清除。
HEALTHCHECK
方式:
1 | # check container health by running a command inside the container |
其中 OPTIONS 可以做如下设置:
--interval=DURATION
(default: 30s)--timeout=DURATION
(default: 30s)--start-period=DURATION
(default: 0s)--retries=N
(default: 3)
docker 容器的健康状况:
starting
:初始状态。healthy
:当有任何一次健康检查通过时unhealthy
:当连续 retries 次健康检查都失败时failed
:单次检查时间超过 timeout
如何写一条 HEALTHCHECK 指令?
本质上是执行一条 shell 命令,不同的返回值,表示不同的含义。
- 0:成功
- 1:失败
- 2:预留,勿用
示例:
1 | HEALTHCHECK --interval=5m --timeout=3s \ |
针对此容器的健康检查,只检查 80 端口的服务,是否能正常返回。
Reference:
Dockerfiles 官网 doc 笔记