Docker 通过 Dockerfile 中的指令来构建镜像。 Docker 镜像由很多镜像构成,每一层对应一个 Dockerfile 里面的指令。 一个运行中的容器,由镜像的所有层 加上可写层 构成,所有的读写都在最上面的可写层。 容器应该是无状态的,销毁、重建应该花费最小的配置。
(build context)构建上下文 1 docker build -f ~/Dockerfile.hi context-dir
可通过编写 .dockerignore
文件,并放置在构建上下文的目录下,达到类似于 .gitignore
的效果,将不必要的文件(夹)不发送给 Docker Daemon。
有如下几种从 stdin
构建镜像的方式:
-
表是占位,从 stdin
读取
无构建上下文(不需要拷贝文件到镜像中)
模板
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 中使用 COPY
、 ADD
,这样会导致构建失败。
传递本地构建上下文
模板
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 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 builderWORKDIR /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
阶段的镜像。
1 docker build --target builder -t alexellis2/href-counter:latest .
后续的 FROM
可以以前面的 FROM
构建的镜像做为 base。
1 2 3 4 5 6 7 8 9 10 FROM alpine:latest as builderRUN apk --no-cache add build-base FROM builder as build1COPY source1.cpp source.cpp RUN g++ -o /binary source.cpp FROM builder as build2COPY 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
三个指令添加层数,其他的指令只会创建临时的层,并不会增加镜像大小。所以,分多个阶段来构建镜像,只拷贝需要的文件到镜像中,能有效减少镜像的大小。
为多行参数排序。按字母顺序排序。避免包重复。如下:
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
来实现多阶段构建
可以为构建阶段添加别名,在后续 FROM
和 COPY --from=<name|index>
中使用
ARG
的使用样例
1 2 3 4 5 6 ARG CODE_VERSION=latestFROM 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 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="" LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12" 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
RUN 两种形式
1 2 3 4 5 RUN <command > 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
指令中
一个比较推荐的写法
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 声明运行时容器提供服务端口,这只是一个声明 ,在运行时并不会因为这个声明应用就会开启这个端口的服务。这个指令的好处为:
方便镜像使用者理解以配置端口映射
运行时,使用 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 ubuntuENTRYPOINT top -b
构建完成后,执行下面的命令,可以看到关闭容器花费了非常长的时间:
当执行 docker stop
时,容器并未完全退出, docker stop
会在超时后,发送 SIGKILL
关闭容器中的其他进程。
如果 top -b
是 PID 1 进程呢?修改一下 Dockerfile 如下:
1 2 FROM ubuntuENTRYPOINT exec top -b
可以很明显看出,很顺畅地就完成了 docker stop
ENTRYPOINT 中的命令默认 PID 为 1,即不会通过某种 shell 来启动(也就无法进行变量替换)。 如果是以一个脚本来启动服务,需要在脚本中使用 exec 或 gosu 来执行。
什么是 gosu?
什么是 exec?
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 2 VOLUME /var/log VOLUME /var/log /var/db
USER 含义:指定镜像运行时的用户和用户组(不指定默认为 root)。USER
指令之后的 CMD
、RUN
、ENTRYPOINT
都将以 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
工作流程:
遇到 ONBUILD 指令,添加触发器到镜像的 metadata 中,所以 ONBUILD 不影响当前构建的镜像。
在构建完成后,触发器会被添加到镜像的 manifest 中,可以通过 docker inspect
进行查看。
当被用作基础镜像(FROM xxx)后,新构建执行触发器,即 ONBUILD 指令,全部成功执行完后,才开始新镜像的构建。
触发器在执行后会被清除。
HEALTHCHECK 方式:
1 2 3 4 HEALTHCHECK [OPTIONS] CMD command 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 命令,不同的返回值,表示不同的含义。
示例:
1 2 HEALTHCHECK --interval=5m --timeout =3s \ CMD curl -f http://localhost/ || exit 1
针对此容器的健康检查,只检查 80 端口的服务,是否能正常返回。
Reference:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
https://docs.docker.com/engine/reference/builder/