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. 无构建上下文(不需要拷贝文件到镜像中)

模板

1
docker build [OPTIONS] -

示例

1
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -

1
2
3
4
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF

上述两种方式,都不会给 Docker Daemon 发送构建上下文,但是注意不要在 Dockerfile 中使用 COPYADD ,这样会导致构建失败。

  1. 传递本地构建上下文

模板

1
docker build [OPTIONS] -f- PATH

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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
  1. 传递远程构建上下文

模板

1
docker build [OPTIONS] -f- PATH

示例

1
2
3
4
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 构建一个镜像,在镜像之间的拷贝操作变得容易。如下:

1
2
3
4
5
6
7
8
9
10
11
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 构建的镜像命名:

1
2
3
4
5
6
7
8
9
10
11
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 中产生的镜像

其它用法

  1. 只构建指定的构建阶段,即某特定 FROM 所代表的镜像。沿用前面的例子,可以只构建 builder 阶段的镜像。
1
docker build --target builder -t alexellis2/href-counter:latest .
  1. 后续的 FROM 可以以前面的 FROM 构建的镜像做为 base。
1
2
3
4
5
6
7
8
9
10
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 ... 。不做显式声明,默认可能会利用构建缓存,能成功利用构建缓存的情况:

  1. 如果从缓存中存在的一个镜像开始构建,那么会将基于此父镜像的所有子镜像的 Dockerfile 拉出来做一个对比,看下一条指令是否相同,如果不相同,缓存失效。
  2. 对比 Dockerfile 内容是否相同。一些特殊命令,需要更多检测。
  3. ADDCOPY ,它们操作的文件对象都会被作为 checksum 的一部分,不一致则缓存失效。
  4. 除了 ADDCOPY ,其他命令带来的文件改变,不会被计入 checksum,也就是在匹配镜像时,会忽略此部分的文件的不同。

其他建议写法

  1. 不安装多余的包。降低复杂度、依赖性、镜像大小和构建时间。

  2. 解耦应用,一个容器只关心一件事情,以达到水平扩展和容器的复用。但是并意味着一个容器一个进程一成不变。

  3. 减少镜像的层数。只有 RUN , COPY , ADD 三个指令添加层数,其他的指令只会创建临时的层,并不会增加镜像大小。所以,分多个阶段来构建镜像,只拷贝需要的文件到镜像中,能有效减少镜像的大小。

  4. 为多行参数排序。按字母顺序排序。避免包重复。如下:

1
2
3
4
5
6
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion

Dockerfile 指令

FROM

三种方式

1
2
3
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 来实现多阶段构建
  • 可以为构建阶段添加别名,在后续 FROMCOPY --from=<name|index> 中使用

ARG 的使用样例

1
2
3
4
5
6
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 应该使用小写字母数字、 .-

-

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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

1
docker image inspect

RUN

两种形式

1
2
3
4
5
# 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 upgradedist-upgrade
  • 使用 apt-get install -y foo 来自动升级一个依赖包
  • RUN apt-get updateapt-get install 包含在同一个 RUN 指令中

一个比较推荐的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

管道符

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 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。这个指令的好处为:

  1. 方便镜像使用者理解以配置端口映射
  2. 运行时,使用 docker run -P 为被 EXPOSE 声明过的容器的端口,随机映射一个宿主机端口。

ENV

单纯设置环境变量

格式有两种:

1
2
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 来展示:

1
2
FROM ubuntu
ENTRYPOINT top -b

构建完成后,执行下面的命令,可以看到关闭容器花费了非常长的时间:

非 PID 1

当执行 docker stop 时,容器并未完全退出, docker stop 会在超时后,发送 SIGKILL 关闭容器中的其他进程。

如果 top -b 是 PID 1 进程呢?修改一下 Dockerfile 如下:

1
2
FROM ubuntu
ENTRYPOINT exec top -b

可以很明显看出,很顺畅地就完成了 docker stop

PID 1

exec form

ENTRYPOINT 中的命令默认 PID 为 1,即不会通过某种 shell 来启动(也就无法进行变量替换)。
如果是以一个脚本来启动服务,需要在脚本中使用 exec 或 gosu 来执行。

  1. 什么是 gosu?
  1. 什么是 exec?

shell form

  • CMD、 docker run 命令行参数都会被忽略掉。
  • ENTRYPOINT 会以 /bin/sh -c 的子进程运行,即非 PID 1 进程,无法接收 UNIX 信号。
  • 可以通过 exec 达到让 ENTRYPOINT 指定的服务 PID 为 1。

ENTRYPOINT & CMD

两者之间的规定:

  1. Dockerfile 中至少有一个 ENTRYPOINT 或 CMD。也就说可以有多个,但是最后一个才会生效
  2. ENTRYPOINT should be defined when using the container as an executable.Why?
  3. CMD 可以用作 ENTRYPOINT 的默认参数 或 for executing an ad-hoc command in a container.
  4. 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
2
VOLUME /var/log
VOLUME /var/log /var/db

USER

含义:指定镜像运行时的用户和用户组(不指定默认为 root)。USER 指令之后的 CMDRUNENTRYPOINT 都将以 USER 指定的用户和用户组运行。
方式:

1
2
USER <user>[:<group>]
USER <UID>[:<GID>]

避免使用 sudo,它会有一定的问题。确有需要考虑使用 gosu

WORKDIR

含义:指定工作目录,建议使用绝对路径(使用相对路径时,会以当前目录中的文件夹为工作目录)。可以根据需要多次指定,避免使用 cd。此指令对后续的 Dockerfile 指令生效,如:RUN, CMD, ENTRYPOINT, COPY, ADD
方式:

1
WORKDIR /path/to/workdir

ONBUILD

含义:构建 BaseImage 时,加入 ONBUILD 指令,会在使用(FROM BaseImage)的构建中,执行 ONBUILD 指令后,才执行此构建的构建指令。
方式:

1
2
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src

使用场景:使用 maven 编译 jar 包,并构建我们自己的业务镜像。所以我们的 Dockerfile 可以如下编写:

1
2
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 指令,如下:

1
2
3
4
5
6
7
8
FROM maven:3-jdk-8

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

ONBUILD ADD . /usr/src/app

ONBUILD RUN mvn install

Example Link

工作流程:

  1. 遇到 ONBUILD 指令,添加触发器到镜像的 metadata 中,所以 ONBUILD 不影响当前构建的镜像。
  2. 在构建完成后,触发器会被添加到镜像的 manifest 中,可以通过 docker inspect 进行查看。
  3. 当被用作基础镜像(FROM xxx)后,新构建执行触发器,即 ONBUILD 指令,全部成功执行完后,才开始新镜像的构建。
  4. 触发器在执行后会被清除。

HEALTHCHECK

方式:

1
2
3
4
# 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:预留,勿用

示例:

1
2
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1

针对此容器的健康检查,只检查 80 端口的服务,是否能正常返回。

Reference:

  1. https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
  2. https://docs.docker.com/engine/reference/builder/