文章目录
Dockerfile reference
Best practices for writing Dockerfiles
Dockerfile 指令详解
镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
Dockerfile 是一个脚本描述文件,其中包含了一条条构建镜像所需的指令和说明。Dockerfile 中的每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
使用 docker build
命令可以根据 Dockerfile 里编排的指令来构建定制docker镜像。大部分指令(如 RUN、COPY)运行于执行 docker build
的构建机;ENTRYPOINT 和 CMD 则运行于 docker run
启动的容器实例。
指令格式
Dockerfile 中典型指令格式如下:
# Comment
INSTRUCTION arguments
-
指令不区分大小写,但建议对指令大写,以便区分指令和一般参数。
-
和 Shell、Python 等脚本一样,以
#
开头的行被视作注释行,不参与执行。- 第一行注释可能是被称为parser directive类似Shebang的解释器指令,目前仅支持指定
escape
指令。
- 第一行注释可能是被称为parser directive类似Shebang的解释器指令,目前仅支持指定
-
Dockerfile 一般以
FROM
指令拉开序幕,指定继承的基础镜像。FROM
指令行之前可能还有parser directive、普通注释或仅供 FROM 指令引用的全局 ARG 参数。
FROM
FROM
指令开创一个新的构建阶段,其后的指令都基于该基础镜像。
FROM [--platform=<platform>] <image> [AS <name>]
# Or
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
# Or
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
- 一个 Dockerfile 中,可能存在多条 FROM 指令,用于构建多个镜像或者存在镜像依赖。
ARG
是唯一允许出现在第一条FROM
指令之前的指令,用于声明仅可被FROM
指令引用的变量。FROM
之后的指令无法引用这个 ARG 参数。
ARG CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD /code/run-app
FROM extras:${CODE_VERSION}
CMD /code/run-extras
ARG & ENV
ARG
为了提高构建定制镜像时的灵活性,可以在 Dockerfile 中通过 ARG varname
前向声明变量 varname(类似C语言中的 extern 变量声明),然后在执行 docker build
打包构建时通过 --build-arg <varname>=<value>
传参。
ARG <name>[=<default value>]
如果 docker build
传入了 --build-arg
参数 varname,但在 Dockerfile 中却没有声明 ARG varname
,将会收到以下提示:
[Warning] One or more build-args [foo] were not consumed.
ENV
在 Dockerfile 中,可通过 ENV
指令设置环境变量(类似 Bash Shell 中的 export varname=value
),该环境变量的生命周期将持续到 docker run
启动镜像的容器实例。
可以通过 docker inspect
查看某个镜像中定义的 ENV
环境变量,也可在 docker run
时通过 --env <key>=<value>
设置环境变量。
如果希望某个环境变量仅在build构建时有效(生命周期不持续到容器实例中),则可考虑在 RUN
指令中设置指令级作用域的环境变量:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...
或使用 ARG
声明配合 docker build --build-arg
传参:
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y ...
WORKDIR
在 Dockerfile 中,可通过 WORKDIR
指令修改当前工作目录,后续 RUN
, CMD
, ENTRYPOINT
, COPY
和 ADD
等指令中的相对(源)目录路径基于 WORKDIR
展开。
相当于 bash shell 中 cd 设置 pwd,即使没有显式指定
WORKDIR
,默认也会指定,可读取引用。
WORKDIR /path/to/workdir
COPY & ADD
COPY
COPY
指令用于将构建机上的资源(src)复制到镜像文件系统(dst)中。
- 构建机:指执行构建命令(
docker build --file ./Dockerfile
)所在主机。 - 资源:包括文件和目录,默认相对 Dockerfile 所在的目录。
- 当目标路径不存在时会自动创建。
COPY
指令有两种写法,当路径中包含空格时,建议采用第二种方式:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
<src>
可能包含通配符(wildcards),遵循Go语言的filepath.Match匹配法则。
ADD
ADD
指令用于将构建机或远程主机上的资源复制到镜像文件系统中。
ADD
指令有两种写法,当路径中包含空格时,建议采用第二种方式:
ADD [--chown=<user>:<group>] [--checksum=<checksum>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
<src>
可能包含通配符(wildcards),遵循Go语言的filepath.Match匹配法则。
difference
虽然 ADD
和 COPY
指令的功能相似,区别在于 COPY
指令仅支持将本地资源拷贝到镜像,而 ADD
增强支持下载远程主机资源。
一般建议优先采用 COPY
指令,因为 COPY
指令更加透明,ADD
指令的一些特性(例如下载、解压过程)则没有那么透明。
如果你需要拷贝多个资源,建议分多条指令分别 COPY
,而不要在一条指令中同时拷贝多个资源,这样能保证独立文件最小缓存。
例如:
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
相比 COPY . /tmp/
放在 RUN 之前而言,以上例子将使 RUN 命令使用更小的缓存更新。
另外,由于镜像大小的限制,需要下载远程资源时,非必要不建议采用 ADD
,建议直接使用 curl
和 wget
。
RUN
RUN
指令用于在当前镜像基础上执行新层命令。
RUN
指令有两种写法:
RUN <command>
(shell form, the command is run in a shell, which by default is/bin/sh -c
on Linux orcmd /S /C
on Windows)RUN ["executable", "param1", "param2"]
(exec form)
shell form
shell 形式的 RUN 指令中的命令默认执行的shell环境可以通过 SHELL 命令修改。
在 shell 形式的 RUN 指令中,可使用反斜杠(\
)进行跨行书写(续行),考虑以下两行:
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'
两个续行合并起来的效果等同于一下一行:
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
exec form
exec 形式的 RUN 指令可避免shell字符串歧义,可以指定基础镜像中不存在的shell命令。
exec 形式可以指定希望的shell,如果想使用不同的shell环境(默认是 /bin/sh
),例如以下指定在 /bin/bash
下执行命令 echo hello
:
RUN ["/bin/bash", "-c", "echo hello"]
difference
exec 形式通常解析为 JSON 数组,这也意味着必须使用双引号而不是单引号。
不同于 shell 形式,exec 形式不会新建 command shell,这意味着一些常规的shell处理可能不会执行。例如 RUN [ "echo", "$HOME" ]
,不会执行shell变量HOME的替换,而是当成普通字符串。
ENTRYPOINT & CMD
CMD
CMD
用于指定在镜像启动后的容器主进程的启动命令,这个“命令”必须是镜像内的软件。
CMD
指令有三种写法:
CMD ["executable","param1","param2"]
(exec form, this is the preferred form)CMD ["param1","param2"]
(as default parameters to ENTRYPOINT)CMD command param1 param2
(shell form)
一般情况下,一个 Dockerfile 中只能有一条 CMD
指令,如果有多条则只有最后一条生效(前面的将被覆盖)。
可以指定 executable
(方式1),也可不指定 executable
(方式2),不指定时一般会在前面声明一条 ENTRYPOINT
指令,此时的作用是为 ENTRYPOINT
提供参数。当 ENTRYPOINT
和 CMD
搭配使用时,注意都要是 JSON 数组形式,往往用作 service-based daemon 镜像(容器)。
如果使用 shell 格式的话,实际的命令会被包装为 /bin/sh -c
的参数的形式执行:
FROM ubuntu
CMD service nginx start
CMD service nginx start
会被理解为 CMD [ "/bin/sh", "-c", "service nginx start"]
,此时的主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了;sh 作为主进程退出了,容器就会退出。
如果希望以后台守护进程形式启动 nginx 服务,可使用JSON数组形式的 CMD
,第一个参数指定 nginx 命令的绝对路径,后续逐个指定参数。这样将直接执行 nginx 命令(nginx -g 'daemon off;'
),启动前台 nginx 服务。
FROM ubuntu
CMD ["nginx", "-g", "daemon off;"]
注意:docker run
命令行参数将会覆盖 CMD
指令,例如 ubuntu 镜像默认的 CMD
是 /bin/bash,执行 docker run -it ubuntu
会直接进入 bash。运行 docker run -it ubuntu cat /etc/os-release
则用 cat /etc/os-release 命令替换默认的 /bin/bash 命令,输出系统版本信息。
ENTRYPOINT
ENTRYPOINT
指令用于配置容器的入口点,和 CMD 一样都是指定容器启动程序及参数。
ENTRYPOINT
和 RUN
指令一样,分为 exec 和 shell 两种格式:
# exec form(preferred)
ENTRYPOINT ["executable", "param1", "param2"]
# shell form
ENTRYPOINT command param1 param2
docker run <image>
的命令行参数,将被添加到 exec 形式的 ENTRYPOINT
的 JSON 数组命令之后,例如 docker run <image> -d
将把 -d
选项(或参数)追加到 ENTRYPOINT
后。这样,给 docker run
带来一些定制传参灵活性。
ENTRYPOINT
和 CMD
一般同时使用JSON数组形式配套使用。此时,ENTRYPOINT
后面的 CMD
的含义就发生了改变,CMD
的内容作为参数传给 ENTRYPOINT
指令,换句话说实际执行时将变为 <ENTRYPOINT> "<CMD>"
。
shell 形式的 ENTRYPOINT
将会阻断后面的 CMD
指令,也不吸纳 docker run
后面的参数。这种形式的缺点是会被启动为 /bin/sh -c
的子命令,这将导致无法接收信号。此时,ENTRYPOINT
的 shell executable 将无法接收到 docker stop <container>
发出的 SIGTERM
信号。
一般情况下,一个 Dockerfile 中只指定一条 ENTRYPOINT
,如果有多条只有最后一条生效(前面的将被覆盖)。运行 docker run
时,也可通过 --entrypoint
参数来指定覆盖 ENTRYPOINT
指令。
interact
既然 CMD
和 ENTRYPOINT
作用类似,有了 CMD
后,为什么还要有 ENTRYPOINT
呢?<ENTRYPOINT> "<CMD>"
这种组合形式可以带来哪些额外的便利呢?
以下描述了两条命令的协作机制:
- Dockerfile 必须指定至少一条
CMD
或ENTRYPOINT
指令。 ENTRYPOINT
可用于将整个容器作为一个 executable 的情形。CMD
可以用作定义ENTRYPOINT
的更多参数,或者在容器中执行一条 ad-hoc 命令。docker run
中的参数可以覆盖CMD
指令。
以下表格总结了 ENTRYPOINT
/ CMD
不同搭配组合形式的含义:
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT [“exec_entry”, “p1_entry”] | |
---|---|---|---|
*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 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 |
注意:ENTRYPOINT
指令将会把基础镜像中定义的 CMD
复位为空,此时需要在当前 Dockerfile 中重新定义 CMD
。
scenario
启动容器就是启动主进程,但有些时候在启动主进程前,需要做一些准备工作。
考虑这样一种场景,容器主进程的作用是启动nginx作为web服务器,但是在启动nginx之前,需要用shell脚本(docker-entrypoint.sh)对 Flutter Web 进行一些预处理。
由于涉及到测试、预发布和发布三个环境,一份Flutter Web构建产物,最终将在集群中部署三个微服务。因此,在 Flutter Web 代码中,需要根据环境变量预埋插桩。在基座配置的 envVars libsonnet 定义环境变量 RUN_MODE,取值分别对应三个环境。
部署后的集群Pod管理的YAML中可看到 containers env 中的 RUN_MODE,容器启动后可读取该环境变量。sh 脚本根据 RUN_MODE 针对不同环境做一些处理,例如替换 CGI Host、替换 <base href>
、开启或关闭vconsole引入模块的注释开关等处理。
以下是 Dockfile,其中 ENTRYPOINT
指令指定入口点为自定义的docker-entrypoint.sh脚本,并带有2个参数。
FROM nginx:latest
# 将docker build构建命令所在主机本地文件(相对该 Dockerfile 所在目录),复制到容器镜像文件系统。
COPY app/scripts/docker/ /usr/local/etc/docker/
COPY app/build/web/ /usr/share/nginx/www/
EXPOSE 80
RUN chmod +x /usr/local/etc/docker/docker-entrypoint.sh
ENTRYPOINT [ "/usr/local/etc/docker/docker-entrypoint.sh", "/usr/share/nginx/www/main.dart.js", "/usr/share/nginx/www/index.html" ]
# 在nginx启动之前,COPY覆盖替换默认的nginx配置
COPY app/scripts/docker/nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]
自定义的docker-entrypoint.sh脚本末尾一句为 exec "${@:3}"
,${@:3}
表示从索引3开始后面的所有参数。
ENTRYPOINT JSON数组有3个参数,CMD JSON数组的3个参数追加到ENTRYPOINT参数后面(索引从3开始),所以 exec "${@:3}"
的意思是将CMD JSON数组中的参数当做命令行执行,即在预处理完成后,执行 nginx -g 'daemon off;'
启动前台nginx服务。