Skip to content

1. 什么是Dockerfile

Dockerfile使用DSL(域特定语言)并包含用于生成Docker映像的指令。Dockerfile将定义快速生成镜像的流程。创建应用程序时,您应该按顺序创建Dockerfile,因为Docker守护进程从上到下运行所有指令.

image-20240619094634927

2. Dockerfile基本组成

官当

2.1 基本结构

Dockerfile 由一行行命令语句组成,并且支持以 # 开头的注释行。

Dockerfile 分为四部分:基础镜像信息维护者信息镜像操作指令容器启动时执行指令

2.2 指令

InstructionDescription
ADDAdd local or remote files and directories.能解压
ARGUse build-time variables.
CMDSpecify default commands.
COPY复制文件或者目录
ENTRYPOINTSpecify default executable.
ENV指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持
EXPOSE服务端容器暴露的端口号
FROMCreate a new build stage from a base image.
HEALTHCHECKCheck a container's health on startup.
LABELAdd metadata to an image.
MAINTAINER指定维护者信息
ONBUILDSpecify instructions for when the image is used in a build.
RUNExecute build commands.
SHELLSet the default shell of an image.
STOPSIGNALSpecify the system call signal for exiting a container.
USERSet user and group ID.
VOLUMECreate volume mounts.
WORKDIRChange 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

为后续的 RUNCMDENTRYPOINT 指令配置工作目录。

可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如

yaml
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

则最终路径为 /a/b/c

ENV

格式为 ENV <key> <value>。 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持。

bash
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。

覆盖EntrypointCmd

如果要覆盖默认的EntrypointCmd,需要遵循如下规则:

  • 如果在容器配置中没有设置 command 或者 args,那么将使用Docker镜像自带的命令及其参数
  • 如果在容器配置中只设置了 command 但是没有设置 args,那么容器启动时只会执行该命令, Docker镜像中自带的命令及其参数会被忽略
  • 如果在容器配置中只设置了 args,那么Docker镜像中自带的命令会使用该新参数作为其执行时的参数
  • 如果在容器配置中同时设置了 commandargs,那么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; topexec ls && top 只会执行 ls 命令