Dockerfiles 官网 doc 笔记
Docker 通过 Dockerfile 中的指令来构建镜像。 Docker 镜像由很多镜像构成,每一层对应一个 Dockerfile 里面的指令。 一个运行中的容器,由镜像的所有层加上可写层构成,所有的读写都在最上面的可写层。 容器应该是无状态的,销毁、重建应该花费最小的配置。
(build context)构建上下文
docker build -f ~/Dockerfile.hi context-dir
通过 -f 指定 Dockerfile 的路径
context-dir 即为构建上下文,该文件夹下面所有的内容都会被传递给 Docker daemon,用来构建镜像。构建上下文包含不相关的内容,会影响构建速度、镜像大小。
可通过编写 .dockerignore 文件,并放置在构建上下文的目录下,达到类似于 .gitignore 的效果,将不必要的文件(夹)不发送给 Docker Daemon。
有如下几种从 stdin 构建镜像的方式:
-表是占位,从stdin读取
- 无构建上下文(不需要拷贝文件到镜像中)
模板
docker build [OPTIONS] -
示例
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
或
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF
上述两种方式,都不会给 Docker Daemon 发送构建上下文,但是注意不要在 Dockerfile 中使用 COPY 、 ADD ,这样会导致构建失败。
- 传递本地构建上下文
模板
docker build [OPTIONS] -f- PATH
示例
# create a directory to work in
mkdir example
cd example
# create an example file
touch somefile.txt
# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF
- 传递远程构建上下文
模板
docker build [OPTIONS] -f- PATH
示例
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c .
EOF
多阶段构建
使 Dockerfile 在容易维护、阅读的基础上,减少镜像的大小
基础用法
在一个 Dockerfile 中使用多个 FROM,每个 FROM 构建一个镜像,在镜像之间的拷贝操作变得容易。如下:
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
对 --from=0 ,按照数组下标从 0 开始,第一个 FROM 构建的镜像为0,依次类推。当然也可以给 FROM 构建的镜像命名:
FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
其实 --from=xxx 中的 xxx 也可以为其它的、不在此 Dockerfile 中产生的镜像
其它用法
- 只构建指定的构建阶段,即某特定
FROM所代表的镜像。沿用前面的例子,可以只构建builder阶段的镜像。
docker build --target builder -t alexellis2/href-counter:latest .
- 后续的
FROM可以以前面的FROM构建的镜像做为 base。
FROM alpine:latest as builder
RUN apk --no-cache add build-base
FROM builder as build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
FROM builder as build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp
构建缓存
显式声明不使用缓存: docker build --no-cache=true ... 。不做显式声明,默认可能会利用构建缓存,能成功利用构建缓存的情况:
- 如果从缓存中存在的一个镜像开始构建,那么会将基于此父镜像的所有子镜像的 Dockerfile 拉出来做一个对比,看下一条指令是否相同,如果不相同,缓存失效。
- 对比 Dockerfile 内容是否相同。一些特殊命令,需要更多检测。
- 对
ADD和COPY,它们操作的文件对象都会被作为 checksum 的一部分,不一致则缓存失效。 - 除了
ADD和COPY,其他命令带来的文件改变,不会被计入 checksum,也就是在匹配镜像时,会忽略此部分的文件的不同。
其他建议写法
不安装多余的包。降低复杂度、依赖性、镜像大小和构建时间。
解耦应用,一个容器只关心一件事情,以达到水平扩展和容器的复用。但是并意味着一个容器一个进程一成不变。
减少镜像的层数。只有 RUN , COPY , ADD 三个指令添加层数,其他的指令只会创建临时的层,并不会增加镜像大小。所以,分多个阶段来构建镜像,只拷贝需要的文件到镜像中,能有效减少镜像的大小。
为多行参数排序。按字母顺序排序。避免包重复。如下:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
Dockerfile 指令
FROM
三种方式
FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
几个要点
FROM开启一个构建Dockerfile的第一个必须是FROM,但是FROM前面可以有ARG来声明变量Dockerfile中可以出现多个FROM来实现多阶段构建- 可以为构建阶段添加别名,在后续
FROM和COPY --from=<name|index>中使用
ARG 的使用样例
ARG CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD /code/run-app
FROM extras:${CODE_VERSION}
CMD /code/run-extras
LABEL
注意事项
- 字符串中含有空格,必须转义或使用
"" - 字符串中含有
",必须转义 - 用自己的反转域名作为 label 前缀,必须对域名有权限
Docker保留的前缀:com.docker.*,io.docker.*,org.dockerproject.*- key 应该使用小写字母数字、
.、-
-
示例
# 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=""
# Set multiple labels on one line
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
查看 label
docker image inspect
RUN
两种形式
# shell form
RUN <command>
# exec form
RUN ["executable", "param1", "param2"]
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指令中
一个比较推荐的写法
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 和 Ubuntu 会自动运行 apt-get clean
管道符
RUN wget -O - https://some.site | wc -l > /number
默认只以最后一个命令 wc 的状态作为整个 RUN 指令的成功或失败,即使 wget 失败了。但可以通过设置 set -o pipefail 避免失败被忽略。如下:
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
单纯设置环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
在 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 来展示:
FROM ubuntu
ENTRYPOINT top -b
构建完成后,执行下面的命令,可以看到关闭容器花费了非常长的时间:

当执行 docker stop 时,容器并未完全退出, docker stop 会在超时后,发送 SIGKILL 关闭容器中的其他进程。
如果 top -b 是 PID 1 进程呢?修改一下 Dockerfile 如下:
FROM ubuntu
ENTRYPOINT exec top -b
可以很明显看出,很顺畅地就完成了 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
含义:直接设置镜像的一个挂载点 用途:存放镜像创建的文件、配置、数据库的数据文件。易变、镜像中的用户数据。 方式:
VOLUME ["/data"]
或
VOLUME /var/log
VOLUME /var/log /var/db
USER
含义:指定镜像运行时的用户和用户组(不指定默认为 root)。USER 指令之后的 CMD、RUN、ENTRYPOINT 都将以 USER 指定的用户和用户组运行。 方式:
USER <user>[:<group>]
USER <UID>[:<GID>]
避免使用 sudo,它会有一定的问题。确有需要考虑使用 gosu。
WORKDIR
含义:指定工作目录,建议使用绝对路径(使用相对路径时,会以当前目录中的文件夹为工作目录)。可以根据需要多次指定,避免使用 cd。此指令对后续的 Dockerfile 指令生效,如:RUN, CMD, ENTRYPOINT, COPY, ADD 方式:
WORKDIR /path/to/workdir
ONBUILD
含义:构建 BaseImage 时,加入 ONBUILD 指令,会在使用(FROM BaseImage)的构建中,执行 ONBUILD 指令后,才执行此构建的构建指令。 方式:
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
使用场景:使用 maven 编译 jar 包,并构建我们自己的业务镜像。所以我们的 Dockerfile 可以如下编写:
FROM maven:3.3-jdk-8-onbuild
CMD ["java","-jar","/usr/src/app/target/demo-1.0-SNAPSHOT-jar-with-dependencies.jar"]
为啥可以这样?这是因为在 maven:3.3-jdk-8-onbuild 这个镜像中,有 ONBUILD 指令,如下:
FROM maven:3-jdk-8
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
ONBUILD ADD . /usr/src/app
ONBUILD RUN mvn install
工作流程:
- 遇到 ONBUILD 指令,添加触发器到镜像的 metadata 中,所以 ONBUILD 不影响当前构建的镜像。
- 在构建完成后,触发器会被添加到镜像的 manifest 中,可以通过
docker inspect进行查看。 - 当被用作基础镜像(FROM xxx)后,新构建执行触发器,即 ONBUILD 指令,全部成功执行完后,才开始新镜像的构建。
- 触发器在执行后会被清除。
HEALTHCHECK
方式:
# check container health by running a command inside the container
HEALTHCHECK [OPTIONS] CMD command
# disable any healthcheck inherited from the base image
HEALTHCHECK NONE
其中 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:预留,勿用
示例:
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
针对此容器的健康检查,只检查 80 端口的服务,是否能正常返回。
Reference: