1. 什么是Dockerfile
Dockerfile使用DSL(域特定语言)并包含用于生成Docker映像的指令。Dockerfile将定义快速生成镜像的流程。创建应用程序时,您应该按顺序创建Dockerfile,因为Docker守护进程从上到下运行所有指令.
2. Dockerfile基本组成
2.1 基本结构
Dockerfile 由一行行命令语句组成,并且支持以 #
开头的注释行。
Dockerfile 分为四部分:基础镜像信息
、维护者信息
、镜像操作指令
和容器启动时执行指令
2.2 指令
Instruction | Description |
---|---|
ADD | Add local or remote files and directories.能解压 |
ARG | Use build-time variables. |
CMD | Specify default commands. |
COPY | 复制文件或者目录 |
ENTRYPOINT | Specify default executable. |
ENV | 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持 |
EXPOSE | 服务端容器暴露的端口号 |
FROM | Create a new build stage from a base image. |
HEALTHCHECK | Check a container's health on startup. |
LABEL | Add metadata to an image. |
MAINTAINER | 指定维护者信息 |
ONBUILD | Specify instructions for when the image is used in a build. |
RUN | Execute build commands. |
SHELL | Set the default shell of an image. |
STOPSIGNAL | Specify the system call signal for exiting a container. |
USER | Set user and group ID. |
VOLUME | Create volume mounts. |
WORKDIR | Change working directory. |
FROM
- 语法
FROM <image>:<tag>
FROM <image>:<tag>
LABEL
LABEL
一般用来添加镜像的 “元数据” ,没有实际作用。常用于声明镜像作者,licensce
等信息,写法为<key>=<value>
,语法为
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
- 查看
# docker image inspect --format='' myimage
{
"com.example.vendor": "ACME Incorporated",
"com.example.label-with-value": "foo",
"version": "1.0",
"description": "This text illustrates that label-values can span multiple lines."
}
# docker image inspect --format='' myimage
{
"com.example.vendor": "ACME Incorporated",
"com.example.label-with-value": "foo",
"version": "1.0",
"description": "This text illustrates that label-values can span multiple lines."
}
WORKDIR
格式为 WORKDIR /path/to/workdir
。
为后续的 RUN
、CMD
、ENTRYPOINT
指令配置工作目录。
可以使用多个 WORKDIR
指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
则最终路径为 /a/b/c
ENV
格式为 ENV <key> <value>
。 指定一个环境变量,会被后续 RUN
指令使用,并在容器运行时保持。
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
ADD
- 语法
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
该命令将复制指定的 <src>
到容器中的 <dest>
。 其中 <src>
可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)
COPY
- 语法
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
复制本地主机的 <src>
(为 Dockerfile 所在目录的相对路径)到容器中的 <dest>
。
当使用本地目录为源目录时,推荐使用 COPY
RUN
- 在
RUN
指令执行过程中,产生的中间镜像会被当做缓存在下一次构建时使用,如果不想使用缓存,使其失效,可以在build
时添加--no-cache
- 尽量把所有的
RUN
指令写到一起,如果是多条shell
命令,可以不用每条命令都添加RUN
,更好的做法是通过\
换行,通过&&
连接多个指令,这样对构建生成的镜像的大小优化是很有帮助的,语法为
RUN set -x && \
yum install -y epel-release \
make \
gcc \
gcc-c++
RUN set -x && \
yum install -y epel-release \
make \
gcc \
gcc-c++
EXPOSE
EXPOSE指令声明了容器在运行时监听指定的网络端口,可以指定端口是监听
TCP还是
UDP,默认为
TCP
EXPOSE
指令实际上并不发布端口,即端口限制,它的作用仅仅是作为构建映像的人和运行容器的人之间的一种文档,关于要发布哪些端口。当运行容器时,要实际发布端口,使用docker
运行中的-p
参数来发布和映射一个或多个端口,或者直接使用-P
来自动随机映射EXPOSE
声明的端口
EXPOSE <port> [<port>/<protocol>...]
EXPOSE <port> [<port>/<protocol>...]
CMD
支持三种格式
CMD ["executable","param1","param2"]
使用exec
执行,推荐方式;CMD command param1 param2
在/bin/sh
中执行,提供给需要交互的应用;CMD ["param1","param2"]
提供给ENTRYPOINT
的默认参数;
指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD
命令。如果指定了多条命令,只有最后一条会被执行。
如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD
指定的命令
ENTRYPOINT
两种格式:
ENTRYPOINT ["executable", "param1", "param2"]
# exec格式,推荐使用ENTRYPOINT command param1 param2
(shell中执行)。
配置容器启动后执行的命令,并且不可被 docker run
提供的参数覆盖。
每个 Dockerfile 中只能有一个 ENTRYPOINT
,当指定多个时,只有最后一个起效
这两种不同的格式有一个很大的区别在于:exec
格式可以接受参数,而shell
格式是会忽略参数的。shell
格式相当于在前面还要再添加/bin/sh -c
,所以app启动的进程ID不是1。
覆盖Entrypoint
与Cmd
如果要覆盖默认的Entrypoint
与Cmd
,需要遵循如下规则:
- 如果在容器配置中没有设置
command
或者args
,那么将使用Docker
镜像自带的命令及其参数 - 如果在容器配置中只设置了
command
但是没有设置args
,那么容器启动时只会执行该命令,Docker
镜像中自带的命令及其参数会被忽略 - 如果在容器配置中只设置了
args
,那么Docker
镜像中自带的命令会使用该新参数作为其执行时的参数 - 如果在容器配置中同时设置了
command
与args
,那么Docker
镜像中自带的命令及其参数会被忽略。 容器启动时只会执行配置中设置的命令,并使用配置中设置的参数作为命令的参数
镜像 Entrypoint | 镜像 Cmd | 容器 command | 容器 args | 命令执行 |
---|---|---|---|---|
[/ep-1] | [foo bar] | [ep-1 foo bar] | ||
[/ep-1] | [foo bar] | [/ep-2] | [ep-2] | |
[/ep-1] | [foo bar] | [zoo boo] | [ep-1 zoo boo] | |
[/ep-1] | [foo bar] | [/ep-2] | [zoo boo] | [ep-2 zoo boo] |
2.3 镜像启动
编写完成 Dockerfile 之后,可以通过 docker build
命令来创建镜像。
基本的格式为 docker build [选项] 路径
,该命令将读取指定路径下(包括子目录)的 Dockerfile,并将该路径下所有内容发送给 Docker 服务端,由服务端来创建镜像。因此一般建议放置 Dockerfile 的目录为空目录。也可以通过 .dockerignore
文件(每一行添加一条匹配模式)来让 Docker 忽略路径下的目录和文件。
docker build -t images:v1 .
docker build -t images:v1 .
❌ 注意
exec 格式的 ENTRYPOINT 或 CMD 就是它们实际在 docker 镜像中的样子,可用 docker inspect image 查看
exec 格式是一种数组形式
,该格式的 ENTRYPOINT 能接收 CMD 或 dock run image 后的参数作为附加参数,相当于是往这个数组中附加元素。
ENTRYPOINT ["echo", "Hello"]
docker run test World and China
输出
Hello World and China
shell 格式可用变量而 exec 格式不一定行
ENTRYPOINT java $JAVA_OPTS -jar /app.jar
docker run -e JAVA_OPTS="-Xms2G" test
ENTRYPOINT java $JAVA_OPTS -jar /app.jar
docker run -e JAVA_OPTS="-Xms2G" test
exec 格式的写法
ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "/app.jar"]
docker run -e JAVA_OPTS="-Xms2G" test
Error: Could not find or load main class $JAVA_OPTS
ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "/app.jar"]
docker run -e JAVA_OPTS="-Xms2G" test
Error: Could not find or load main class $JAVA_OPTS
FROM debian:11
ENV TIME_ZOME Asia/Shanghai
ENV LANG en_US.utf8
ENV DOTNET_ROOT /usr/local/dotnet
ENV PATH $PATH:$DOTNET_ROOT
ADD dotnet-sdk-8.0.100-linux-x64.tar.gz /usr/local/dotnet
ADD node-v16.20.2-linux-x64.tar.gz /usr/local
COPY ["sources.list", "/etc/apt"]
RUN echo "${TIME_ZOME}" > /etc/timezone && \
ln -sf /usr/share/zoneinfo/${TIME_ZOME} /etc/localtime && \
ln -sf /usr/local/node-v16.20.2-linux-x64/bin/node /usr/local/bin && \
ln -sf /usr/local/node-v16.20.2-linux-x64/bin/npm /usr/local/bin && \
apt update && \
apt -y install net-tools libicu-dev && \
apt clean
FROM debian:11
ENV TIME_ZOME Asia/Shanghai
ENV LANG en_US.utf8
ENV DOTNET_ROOT /usr/local/dotnet
ENV PATH $PATH:$DOTNET_ROOT
ADD dotnet-sdk-8.0.100-linux-x64.tar.gz /usr/local/dotnet
ADD node-v16.20.2-linux-x64.tar.gz /usr/local
COPY ["sources.list", "/etc/apt"]
RUN echo "${TIME_ZOME}" > /etc/timezone && \
ln -sf /usr/share/zoneinfo/${TIME_ZOME} /etc/localtime && \
ln -sf /usr/local/node-v16.20.2-linux-x64/bin/node /usr/local/bin && \
ln -sf /usr/local/node-v16.20.2-linux-x64/bin/npm /usr/local/bin && \
apt update && \
apt -y install net-tools libicu-dev && \
apt clean
3. 自特点缺点
exec 格式要求一个坑一个参数,所以像上面见到的那样无法在中间动态插入参数,比如不能在中间某一个位置上写上 "-Xmx5G -Xms2G", 这分明是两个参数,只能在后面附加参数
shell 格式由于命令总是由 "/bin/sh -e" 启动的子进程,它不是 PID 1 超级进程,从而无法收到 Unix 的信号,自然不能收到从 docker stop <container>
发来的 SIGTERM
信号。
简述一下 docker stop <container>
工作原理,它向容器中的 PID 为 1 进程发送 SIGTERM 信号,并给予 10 秒钟(可用参数 --time) 清理,超时才 -9 强杀,这样可以比较优雅的关闭容器。"/bin/sh -e" 是一个 PID 1 进程,它收到了 SIGTERM 却不会转发给它的子命令,这样就造成了 "/bin/sh -e" 收到 SIGTERM 未作响应被强杀,同时把它的子进程毫无征兆的干掉了。像在 Java 中用 Runtime.addShutdownHook()
是捕获不到该信号的。
4. 增强型shell格式
这里补充一种 ENTRYPOINT 的声明格式,它实质是 shell 格式,为而把它单独列出来关键就在于 shell 的 exec
命令。此 exec
非前面 exec 格式中的 exec, 而是一个结结实实的 shell 命令。
ENTRYPOINT exec command param1 param2 ...
比如:
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
它仍然是 shell 格式,所以 inspect 镜像后看到的 ENTRYPOINT 是
ENTRYPOINT ["/bin/sh", "-c" "exec java $JAVA_OPTS -jar /app.jar"]
然而加了 exec
的绝妙之处在于:
shell 的内建命令 exec 将并不启动新的shell,而是用要被执行命令替换当前的 shell 进程,并且将老进程的环境清理掉,exec 后的命令不再是 shell 的子进程序,而且 exec 命令后的其它命令将不再执行。从执行效果上可以看到 exec 会把当前的 shell 关闭掉,直接启动它后面的命令。
虽然它与之后的命令(如上 exec java $JAVA_OPTS -jar /app.jar
)还是作为 "/bin/sh" 的第二个参数,但 exec
来了个金蝉脱壳,让这里的 java
进程得已作为一个 PID 1 的超级进程,进行使得这个 java 进程可以收到 SIGTERM 信号。或者理解 exec
为 "/bin/sh" 的子进程,但是借助于 exec
让它后面的进程启动在最顶端。
另外,由于通过 "/bin/sh" 的搭桥,命令中的变量(如 $JAVA_OPTS) 也会被正确解析,因此 ENTRYPOINT exec command param1 param2 ...
是被推荐的格式。
注意:exec 只会启动后面的第一个命令,exec ls; top
或 exec ls && top
只会执行 ls
命令