Dockerfile
是Docker用来构建镜像的文本文件,包含自定义的指令和格式。可以通过docker build
命令利用Dockerfile
构建镜像。Dockerfile
提供了一系列统一的资源配置语法指令,开发人员可以根据需求定制Dockerfile
,然后使用这份Dockerfile
文件进行自动化镜像构建,简化了构建镜像的复杂过程,同时Dockerfile
与镜像配合使用,使Docker在构建时可以充分利用镜像的功能进行缓存,大大提升了Docker的使用效率。
本文主要对Dockerfile指令和使用Dockerfile构建镜像进行简单总结。
Dockerfile
由一行行命令语句组成,支持以#
开头的注释行。一般Dockerfile
主体分为基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令四个部分。下面是从Docker Hub上拿来的nginx的一个Dockerfile的例子,可以对Dockerfile的基本结构有个大体的了解,我们在编写自己的Dockerfile时也可以参考Docker Hub上优秀镜像的Dockerfile,通过优秀镜像的Dockerfile来学习和总结经验来编写高效的Dockerfile文件。
#指定所构建镜像的基础镜像
FROM debian:buster-slim
#指定生成镜像的元数据标签信息
LABEL maintainer="NGINX Docker Maintainers <[email protected]>"
#指定环境变量
ENV NGINX_VERSION 1.18.0
ENV NJS_VERSION 0.4.0
ENV PKG_RELEASE 1~buster
#运行指定命令
RUN set -x \
# create nginx user/group first, to be consistent throughout docker variants
&& addgroup --system --gid 101 nginx \
&& adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
&& apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \
&& \
NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
found=''; \
for server in \
ha.pool.sks-keyservers.net \
hkp://keyserver.ubuntu.com:80 \
hkp://p80.pool.sks-keyservers.net:80 \
pgp.mit.edu \
; do \
echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
done; \
test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \
&& dpkgArch="$(dpkg --print-architecture)" \
&& nginxPackages=" \
nginx=${NGINX_VERSION}-${PKG_RELEASE} \
nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-${PKG_RELEASE} \
" \
&& case "$dpkgArch" in \
amd64|i386) \
# arches officialy built by upstream
echo "deb https://nginx.org/packages/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \
&& apt-get update \
;; \
*) \
# we're on an architecture upstream doesn't officially build for
# let's build binaries from the published source packages
echo "deb-src https://nginx.org/packages/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \
\
# new directory for storing sources and .deb files
&& tempDir="$(mktemp -d)" \
&& chmod 777 "$tempDir" \
# (777 to ensure APT's "_apt" user can access it too)
\
# save list of currently-installed packages so build dependencies can be cleanly removed later
&& savedAptMark="$(apt-mark showmanual)" \
\
# build .deb files from upstream's source packages (which are verified by apt-get)
&& apt-get update \
&& apt-get build-dep -y $nginxPackages \
&& ( \
cd "$tempDir" \
&& DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
apt-get source --compile $nginxPackages \
) \
# we don't remove APT lists here because they get re-downloaded and removed later
\
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)
&& apt-mark showmanual | xargs apt-mark auto > /dev/null \
&& { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \
\
# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)
&& ls -lAFh "$tempDir" \
&& ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \
&& grep '^Package: ' "$tempDir/Packages" \
&& echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \
# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes")
# Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
# ...
# E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
&& apt-get -o Acquire::GzipIndexes=false update \
;; \
esac \
\
&& apt-get install --no-install-recommends --no-install-suggests -y \
$nginxPackages \
gettext-base \
curl \
&& apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
\
# if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
&& if [ -n "$tempDir" ]; then \
apt-get purge -y --auto-remove \
&& rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
fi
# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
# make default server listen on ipv6
RUN sed -i -E 's,listen 80;,listen 80;\n listen [::]:80;,' \
/etc/nginx/conf.d/default.conf
#指定镜像内服务所监听的端口
EXPOSE 80
#指定所创建镜像启动的容器接收退出的信号值
STOPSIGNAL SIGTERM
#指定启动容器时默认执行的命令
CMD ["nginx", "-g", "daemon off;"]
Dockerfile
的格式如下:
# Comment
INSTRUCTION arguments
其中指令(INSTRUCTION)不区分大小写,但是为了与参数区分,通常情况大写。Docker会按顺序运行Dockerfile
中的指令。Dockerfile
必须以FROM
指令开头。它可以在解析器指令(parser directives),注释(comments)和全局范围参数(globally scoped ARGs)之后。FROM
指令具体指定你想要构建的镜像的父镜像(Parent Image)。FROM
指令只能在一个或多个声明在Dockerfile
中的FROM
行中使用的参数即ARG
指令之前。
Docker将以#
开头的行视为注释,除非该行是有效的解析器指令(parser directive)。在一行中的任何其他位置使用#
标记都将被视为参数,它允许下面这样的语句。还有注释中不支持行连续字符(\
)。
# Comment
RUN echo 'we are running some # of cool things'
解析器指令是可选的,并且会影响Dockerfile
中后续行的处理方式。解析器指令不会添加层到构建的镜像中,也不会显示为一个构建步骤。解析器指令以#directive = value
的形式编写为特殊类型的注释,单个指令只能使用一次。
一旦一个注释,空行或者构建指令被执行。Docker就不会再寻找解析器指令。相反它会将任何符合解析器指令的格式视为注释,并且不会去尝试验证它是否是一个解析器指令,因此所有的解析器指令都必需位于Dockerfile
的顶部。
解析器指令不区分大小写,但是它们通常是小写的并且在每个解析器指令后会使用空行分隔,解析器指令不支持行连续字符(\
),解析器指令允许非断行空格字符,根据上述规则,以下示例均不是有效的解析器指令:
使用行连续字符(\
)无效:
# direc \
tive=value
出现两次无效:
# directive=value1
# directive=value2
FROM ImageName
出现在构建器指令后被视为注释:
FROM ImageName
# directive=value
出现在注释后被视为注释而不是解析器指令:
# About my dockerfile
# directive=value
FROM ImageName
未被识别的未知指令会被视为注释,而出现在其后的有效指令也会被视为注释:
# unknowndirective=value
# knowndirective=value
解析器指令支持syntax
和escape
。
# syntax=[remote image reference]
# syntax=docker/dockerfile
# syntax=docker/dockerfile:1.0
# syntax=docker.io/docker/dockerfile:1
# syntax=docker/dockerfile:1.0.0-experimental
# syntax=example.com/user/repo:tag@sha256:abcdef...
只有使用BuildKit后端时才启用此功能,syntax
指令设定用于构建当前Dockerfile的Dockerfile构建器的位置。BuildKit后端允许无缝使用以Docker镜像的形式分发并在容器沙箱环境中执行的构建器的外部实现。
自定义的Dockerfile实现能够:
Docker分发了可用于docker/dockerfile
在Docker Hub上的仓库下构建Dockerfile的镜像的正式版本。这里有稳定版(stable)和测试版(experimental)两个发布新镜像的渠道。
稳定版渠道遵循的版本控制示例如下:
docker/dockerfile:1.0.0
- 只允许不可变版本 1.0.0
docker/dockerfile:1.0
- 允许版本 1.0.*
docker/dockerfile:1
- 允许版本 1.*.*
docker/dockerfile:latest
- 稳定渠道发布的最新版本测试版渠道在正式发布时使用稳定版渠道中主要和次要组成部分的增量版本控制的示例如下:
docker/dockerfile:1.0.1-experimental
- 只允许不可变版本1.0.1-experimental
docker/dockerfile:1.0-experimental
- 1.0
版本之后最新发布的测试版docker/dockerfile:experimental
- 测试渠道发布的最新版本我们可以根据需求选择合适的发布渠道,如果只想修正错误,则应使用docker/dockerfile:1.0
,如果想从测试功能中受益,则应使用测试版渠道,如果正在使用测试版渠道,则较新的版本可能无法向后兼容,因此建议使用不可变的完整版本。
# escape=\ (backslash)
或 # escape=` (backtick)
escape
指令设定在Dockerfile
中作为转义字符的字符,如果未指定,默认转义字符为\
。
转义字符既用于转义行中的字符,也用于转义换行符,这允许Dockerfile
指令跨越多行。注意,无论escape
解析器指令是否包含在Dockerfile
中,都不会在RUN
命令中执行转义,除非在行尾。
将转义字符设置为`
在Windows
上特别有用,其中\
是目录路径分隔符。`
与Windows PowerShell一致。
通过以下示例可以对escape
指令做一个简单的了解,该示例在Windows
系统环境中会运行失败,第二行末尾的第二个\
将被解释为换行符的转义符,而不是第一个\
的转义目标,类似的第三行结尾的 \
将会被作为换行符处理,这个dockerfile的结果就是第二行和第三行被认为是单个指令:
FROM microsoft/nanoserver
COPY testfile.txt c:\\
RUN dir c:\
上面的一个解决方案是使用/
作为COPY
指令和dir
的目标。但是这种语法对于Windows
上的路径来说并不自然,并且最坏的情况是由于Windows
上的所有命令都不支持/
作为路径分隔符,因此容易出错。
但是通过添加escape
解析器指令,以下Dockerfile
会在Windows
上按照预期的方式的成功使用自然路径执行,具体方式如下:
# escape=`
FROM microsoft/nanoserver
COPY testfile.txt c:\
RUN dir c:\
用ENV
声明的环境变量也可以在Dockerfile
的某些指令中作为参数使用,还可以处理转义,将字符串中的类似变量的语法包含在语句中。
在Dockerfile
中使用$variable_name
或${variable_name}
标注环境变量具有相同的效果,并且括号语法通常用来解决没有空格的变量名称的问题,例如${foo}_bar
。
${variable_name}
语法还支持以下指定的一些标准bash
修饰符,下面的word
在所有情况下可以是包括其他环境变量的任何字符串:
${variable:-word}
:如果设置了variable
,结果将是该值,如果未设置variable
,结果将是word
。${variable:+word}
:如果设置了variable
,结果将是word
,否则结果是空字符串 。我们还可以通过在变量前添加\
来进行转义。例如\$foo
或\${foo}
,将分别转换为$foo
和${foo}
文字,下面是一个应用示例,#
之后显示的是解析后的表示:
FROM busybox
ENV foo /bar
WORKDIR ${foo} # WORKDIR /bar
ADD . $foo # ADD . /bar
COPY \$foo /quux # COPY $foo /quux
另外Dockerfile
中支持环境变量的有以下指令:ADD
、COPY
、ENV
、EXPOSE
、FROM
、LABEL
、STOPSIGNAL
、USER
、VOLUME
、WORKDIR
、ONBUILD
。
注意:环境变量替换将在整个指令中为每个变量使用相同的值。例如:
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc
def
的值为hello
不是bye
,然而ghi
的值为bye
,这是因为它不是将abc
设置为bye
的相同指令的一部分。
Dockerfile
的格式上文已经总结过了,其中用于构建镜像的构建指令一般格式为INSTRUCTION arguments
,而Dockerfile
中一般有以下构建指令:FROM
、LABEL
、MAINTAINER
、EXPOSE
、ENV
、ENTRYPOINT
、VOLUME
、USER
、WORKDIR
、ARG
、ONBUILD
、STOPSIGNAL
、HEALTHCHECK
、SHELL
、RUN
、CMD
、ADD
、COPY
。下面将对这些指令进行简单总结。
FROM [--platform=
或
FROM [--platform=
或
FROM [--platform=
FROM
指令初始化一个新的构建阶段并为后续指令设置基础镜像。因此有效的Dockerfile
必须以FROM
指令开头,其中基础镜像可以是任何有效镜像,而通过从公有仓库中拉取镜像来启动它尤其容易。
--platform
选项在FROM
引用了多平台镜像的情况下可以用于指定镜像所属的平台。例如linux/amd64
,linux/arm64
或windows/amd64
,默认会使用发起构建请求的目标平台,也可以在该选项的值中使用全局构建参数。
ARG
是Dockerfile
中唯一可以在FROM
之前的指令。
FROM
可以在单个Dockerfile
中多次出现以创建多个镜像,或者使用一个构建阶段作为另一个构建阶段的依赖项。这只需在每个新的FROM
指令之前记下提交输出的最后一个镜像ID。每个FROM
指令会清除先前指令创建的任何状态。
通过将AS name
添加到FROM
指令可以将可选的名称赋予到新的构建阶段,该名称可以在后续的FROM
和COPY --from=
指令中使用,以引用此阶段构建的镜像。
tag
或digest
的值是可选的,如果省略其中任何一个,构建器默认采用latest
标签,如果找不到tag
值,构建器将返回错误。
FROM
指令支持在第一个FROM
之前发生的任何ARG
指令声明的变量。示例如下:
ARG CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD /code/run-app
FROM extras:${CODE_VERSION}
CMD /code/run-extras
在FROM
之前声明的ARG
在构建阶段之外,因此在FROM
之后的任何指令中都不能使用它。要使用在第一个FROM
之前声明的ARG
的默认值,需在构建阶段内使用没有值的ARG
指令。示例如下:
ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version
LABEL
LABEL
指令将元数据添加到生成的镜像中,采用键值对的形式。
想要在LABEL
值中包含空格需要像在命令行解析中一样使用引号和反斜杠。示例如下:
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,可以在一行中指定多个label。在Docker 1.10之前,这减小了最终镜像的大小,但现在不再是这样了。我们仍然可以选择在单个指令中指定多个label,有以下两种方式:
LABEL multi.label1="value1" multi.label2="value2" other="value3"
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
基础或父镜像(FROM
指定镜像)中包含的label将继承到新构建的镜像,如果label已存在但具有不同的值,则最近应用的值将覆盖任何之前设置的值。
使用docker image inspect
命令可以查看镜像的label,使用--format
选项可以只显示label,例如命令docker image inspect --format='' myimage
。
MAINTAINER
MAINTAINER
指令设置生成镜像的作者(Author)字段。不过LABEL
指令使用起来比MAINTAINER
更灵活,可以设置我们所需的任何元数据并且能够轻松查看这些元数据,所以比起MAINTAINER
指令更推荐使用LABEL
指令。要设置与MAINTAINER
字段对应的label,可以使用下面示例中的LABEL
指令:
LABEL maintainer="[email protected]"
该MAINTAINER
字段可以通过docker inspect
与其他label一起显示出来。
EXPOSE
EXPOSE
指令通知Docker容器在运行时监听指定的网络端口,可以指定端口是监听TCP还是UDP,如果未指定网络协议,默认监听TCP。
EXPOSE
指令只是起到声明作用,并不会自动完成端口映射,也就是实际上不会发布端口。
如果想要在运行容器时实际发布端口,即完成端口映射,需要在docker run
即创建运行容器时使用-p
具体指定一个或多个主机端口与容器端口间的映射,或者使用-P
,对容器中被监听的每个端口,Docker都将自动分配一个主机临时端口与之完成映射。
默认情况EXPOSE
指定端口是监听TCP,也可以指定为UDP,例如markup EXPOSE 80/udp
。如果要指定端口是同时监听TCP和UDP,需要包括两行指令,下面是一个示例,在这种情况如果将-P
与docker run
一起使用,则端口将对TCP和UDP各暴露一次,由于-P
使用的是Docker自动分配的主机临时端口,所以在TCP和UDP上与EXPOSE
指定端口映射的主机端口不相同。
EXPOSE 80/tcp
EXPOSE 80/udp
无论EXPOSE
如何设置,在运行时都可以使用-p
进行覆盖。示例如下:
docker run -p 80:80/tcp -p 80:80/udp ...
ENV
或
ENV
ENV
指令指定一个环境变量
的值
,该值将存在于构建阶段中所有后续指令的环境中,并且可以在许多时候进行内部替换。
ENV
指令有两种形式,第一种ENV
设置单个变量的值,第一个空格后面的整个字符串包括空格字符都将被视为
,因为该值将针对其他环境变量进行解释,在未对其进行转义的情况下将删除引号字符。示例如下:
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy
第二种形式ENV
允许一次设置多个变量,注意与第一种形式不同的是第二种形式在语法中使用等号(=)。与命令行解析一样,引号和反斜杠可用于在值内包含空格。示例如下,该例在最终生成的镜像中与上例一样会产生相同的结果。
ENV myName="John Doe" myDog=Rex\ The\ Dog \
myCat=fluffy
从生成的镜像运行容器时使用ENV
设置的环境变量将保持不变,可以使用docker inspect
来查看环境变量的值,还可以使用docker run --env
来改变指定环境变量的值。
环境变量的持久性可能会导致意想不到的副作用。例如设置ENV DEBIAN_FRONTEND noninteractive
可能会使基于Debian的镜像上的apt-get用户感到困惑。要为单个命令设置值,可以使用RUN
。
ENTRYPOINT
指令有两种形式:
ENTRYPOINT ["executable", "param1", "param2"]
(exec形式,推荐形式)ENTRYPOINT command param1 param2
(shell形式)ENTRYPOINT
指令指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有传入值作为该命令的参数。
使用exec形式时docker run
传入的命令行参数会被附加到ENTRYPOINT
所有元素之后,并将覆盖CMD
指定的所有元素。这允许将参数传递给入口点,即docker run
将-d
参数传递给入口点。使用docker run --entrypoint
可以覆盖ENTRYPOINT
指令。
使用shell形式时ENTRYPOINT
指令会防止任何CMD
或run
命令行参数被使用,但缺点是ENTRYPOINT
将作为/bin/sh -c
的子命令启动,它不传递信号。这意味着进程在容器中的PID
不是1并且不会接收Unix信号,所以命令进程将不会从docker stop
中接收到SIGTERM
信号,即在通过docker stop
的形式停止容器的时候接收不到停止信号将会导致异常终止。
在Dockerfile
中只有最后一个ENTRYPOINT
指令才会生效。
exec形式将被解析为JSON数组,所以必须使用"
来包围单词而不是'
。
exec形式不会调用命令shell,也就不会发生正常的shell处理,例如ENTRYPOINT [ "echo", "$HOME" ]
不会对$HOME
进行变量替换。如果需要shell处理,可以使用shell形式或者直接执行shell,例如ENTRYPOINT [ "sh", "-c", "echo $HOME" ]
。当使用exec形式并直接执行shell时,跟shell形式的情况一样是执行环境变量扩展的shell而不是docker。
如果CMD
在基础镜像中被设置过,设置ENTRYPOINT
将会重置CMD
为一个空值,这种情况下CMD
必须在当前镜像中设置一个值。
CMD
和ENTRYPOINT
指令都定义了运行容器时执行的命令,以下为它们之间协作的规则。
CMD
或ENTRYPOINT
命令。ENTRYPOINT
。CMD
应该作为ENTRYPOINT
命令定义默认参数或在容器中执行特定命令的方法。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 [“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 ["/data"]
VOLUME
指令创建一个具有指定名称的挂载点,并将其标记为从本机主机或其他容器保存的外部挂载卷,该值可以是JSON数组或具有多个参数的普通字符串。使用docker run
命令会用任何存在于基础镜像内指定位置的数据初始化新创建的卷。
以下是关于Dockerfile中的卷的注意事项:
基于Windows容器的卷:使用基于Windows的容器时,容器中卷的目标必须是一个不存在的或空目录和C:
以外的驱动器这两者之一。
从Dockerfile中更改卷:如果任何构建步骤在声明后更改卷内的数据,那么这些更改将被丢弃。
JSON格式:将列表解析为JSON数组必须使用"
而不是'
。
主机目录在容器运行时被声明:主机目录(挂载点)本质上是依赖于主机的。这是为了保持镜像的可移植性,因为不能保证给定的主机目录在所有主机上都可用,因此无法从Dockerfile中挂载主机目录。VOLUME
指令不支持指定host-dir
参数,在创建或运行容器时必须指定挂载点。
USER
或
USER
USER
指令设置用户名或UID以及可选的用户组或GID,当运行镜像以及在Dockerfile
中紧接着的所有RUN
,CMD
和ENTRYPOINT
指令时会使用该指定用户。
当为用户指定用户组时,用户将只有指定的用户组的成员身份,任何其他已配置的用户组的成员身份将被忽略。
当用户没有主用户组时将使用root
组运行镜像或下一条指令。
在Windows上,如果用户不是内置帐户,则必须先创建用户,这可以通过作为Dockerfile一部分调用的net user
命令来完成。示例如下:
FROM microsoft/windowsservercore
# 在容器中创建Windows用户
RUN net user /add patrick
# 为后续命令设置用户名
USER patrick
WORKDIR /path/to/workdir
WORKDIR
指令为Dockerfile
中的任何RUN
、CMD
、ENTRYPOINT
、COPY
和ADD
指令设置工作目录。如果WORKDIR
不存在,即使它没有在任何后续Dockerfile
指令中被使用,也将创建它。
WORKDIR
指令可以在Dockerfile
中多次使用,如果提供了相对路径,则这个工作目录会基于之前WORKDIR
指令指定的路径。示例如下,该Dockerfile
最终的pwd
命令输出为/a/b/c
。
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
WORKDIR
指令可以解析之前使用ENV
设置的环境变量,并且只能使用Dockerfile
中显式设置的环境变量。示例如下,该Dockerfile
最终的pwd
命令输出为/path/$DIRNAME
。
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
ARG
ARG
指令定义了一个变量,在使用docker build
命令构建时通过--build-arg
将该变量传递给构建器。
如果指定了未在Dockerfile
中定义的构建参数,构建时会输出以下警告:
[Warning] One or more build-args [foo] were not consumed.
一个Dockerfile可以包含一个或多个ARG
指令。
不推荐使用构建时变量即ARG
指令定义的变量来传递诸如github密钥,用户凭证等私密数据。使用docker history
命令时任何镜像用户都能看到构建时变量。
ARG
指令可以包含可选的默认值,如果ARG
指令指定了默认值,并且在构建时没有传递值,则构建器将使用默认值。
ARG
变量的定义从Dockerfile
中定义的行开始生效,而不是从命令行或其他地方的参数使用时才生效。因此在ARG
指令定义变量之前,对该变量的任何使用都将导致空字符串,即在ARG
指令定义变量之前使用该变量,该变量的值为空字符串。例如以下示例,如果通过docker build --build-arg user=what_user .
构建镜像,则第二行USER
的值为some_user
,第四行的USER
在设置用户时的值为what_user
,并在命令行上传递了what_user
值。
FROM busybox
USER ${user:-some_user}
ARG user
USER $user
# ...
ARG
指令在它所定义的构建阶段结束超出了作用域,则在多个阶段都要使用arg,即每个阶段必须包含ARG
指令。示例如下:
FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS
FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS
我们可以使用ARG
或ENV
指令指定RUN
指令可用的变量,使用ENV
指令定义的环境变量始终覆盖ARG
指令定义的同名变量。例如以下示例,如果通过docker build --build-arg CONT_IMG_VER=v2.0.1 .
构建镜像,在这种情况下RUN
指令使用的是v1.0.0
而不是用户传递的ARG
设置值v2.0.1
。
FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER v1.0.0
RUN echo $CONT_IMG_VER
与ARG
指令不同的是ENV
值将始终保留在构建的镜像中,而ARG
定义的变量将在镜像编译完成后就不再存在。例如以下示例,如果通过docker build .
不带--build-arg
构建镜像,在这种情况下CONT_IMG_VER
仍然保存在镜像中,但其值为v1.0.0
,因为它是ENV
指令在第3行中的默认设置。
FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
RUN echo $CONT_IMG_VER
Docker有以下一组在Dockerfile中使用而无需相应的ARG
指令的预定义的ARG
变量:HTTP_PROXY
、http_proxy
、HTTPS_PROXY
、https_proxy
、FTP_PROXY
、ftp_proxy
、NO_PROXY
、no_proxy
。要使用这些预定义ARG
变量,只需通过--build-arg
在命令行上传递它们。默认情况下,这些预定义变量将从docker history
的输出中排除,排除它们可降低在HTTP_PROXY
变量中意外泄露敏感验证信息的风险。
ONBUILD
ONBUILD
指令向镜像添加将在稍后执行的触发器指令,当该镜像作为另一个镜像构建的基础时,触发器将在下游构建的上下文中执行,就好像是在下游Dockerfile
中的FROM
指令之后立即插入一样,换句话说,就是ONBUILD
指令向镜像添加了触发器指令,当该镜像(父镜像)作为构建另一个镜像(子镜像)的基础镜像时,在构建另一个镜像(子镜像)时,这些触发器指令就在构建另一个镜像(子镜像)的Dockerfile
的FROM
指令执行时加入到构建过程,看起来就像是触发器指令在FROM
指令之后执行。
任何构建指令都可以注册为触发器,但是不允许使用ONBUILD ONBUILD
这样的链状ONBUILD
指令,即ONBUILD
指令中不能包含ONBUILD
指令,并且ONBUILD
指令可能不会触发FROM
或MAINTAINER
指令。当需要制作一个用来构建其他镜像的基础镜像时,ONBUILD
指令会很有用。
ONBUILD
指令时构建器会将触发器添加到正在构建的镜像的元数据中,该指令不会影响当前构建,即不会在当前构建流程中执行。OnBuild
键下,可以使用docker inspect
查看。FROM
指令将该镜像作为新构建镜像的基础镜像。作为处理FROM
指令的一部分,下游构建器查找ONBUILD
触发器,并按照它们注册的顺序执行它们。如果任何触发器指令执行失败,FROM
指令会被终止,导致构建失败。如果所有触发器指令执行成功,FROM
指令成功执行完毕并且构建会继续正常进行。STOPSIGNAL signal
STOPSIGNAL
指令设置将发送给容器退出的系统调用信号。此信号可以是与内核的系统调用表中的位置匹配的有效无符号数,例如9或像SIGKILL这样的SIGNAME格式的信号名。
HEALTHCHECK
指令有两种形式:
HEALTHCHECK [OPTIONS] CMD command
(通过在容器内运行命令来检查容器的健康状况)HEALTHCHECK NONE
(禁用从基础镜像继承的任何健康检查)HEALTHCHECK
指令告诉Docker如何测试容器以检查它是否仍在工作。可以检测到陷入无限循环且无法处理新连接的Web服务器,即使服务器进程仍在运行等情况。当容器指定了健康检查时,除了正常状态外还有健康状态,此状态最初为starting
,每当健康检查通过时,无论之前处于什么状态它都会变为healthy
,在经过一定数量的连续失败后它会变成unhealthy
。
OPTIONS
支持的参数有:
--interval=DURATION
(默认值:30s
):启动容器到进行健康检查的间隔时间以及两次健康检查的间隔时间。--timeout=DURATION
(默认值:30s
):单次健康检查的超时时间,超过该时间该次健康检查失败。--start-period=DURATION
(默认值:0s
):为需要时间引导的容器提供的初始化时间,在此期间检查失败将不计入最大重试次数,但是如果在启动期间健康检查成功,则会将容器视为已启动,并且所有连续失败将计入最大重试次数。--retries=N
(默认值:3
):健康检查失败后的最大重试次数,重试了最大次数依然失败,容器将被视为unhealthy
。Dockerfile中只能有一个HEALTHCHECK
指令,如果超过一个那只有最后一个HEALTHCHECK
会生效。
CMD
关键字之后的命令可以是shell命令(例如HEALTHCHECK CMD /bin/check-running
)或exec数组(与其他Dockerfile命令一样,详情参考ENTRYPOINT
)。
命令的退出状态指示容器的运行状况,可能的值有:
为了帮助调试错误,命令在stdout或stderr上写入的任何输出文本(UTF-8编码)都将存储在健康状态中并可以使用docker inspect
进行查询,此类输出应该保持简短(目前仅存储前4096个字节)。
当容器的健康状态改变时将使用新的状态生成health_status
事件。
HEALTHCHECK
指令是在Docker 1.12时添加的。
SHELL ["executable", "parameters"]
SHELL
指令允许用于命令的shell形式的默认shell被覆盖。Linux上的默认shell是["/bin/sh", "-c"]
,Windows上是["cmd", "/S", "/C"]
。SHELL指令必须以JSON格式写入Dockerfile。
SHELL
指令在Windows上特别有用,其中有cmd
和powershell
这两个常用且相当不同的本机shell,以及包括sh
的备用shell。
SHELL
指令可以出现多次。每个SHELL
指令覆盖以前的所有SHELL
指令,并影响所有后续指令。示例如下:
FROM microsoft/windowsservercore
# 以 cmd /S /C 执行 echo default
RUN echo default
# 用 cmd /S /C 作为 powershell -command 执行 Write-Host default
RUN powershell -command Write-Host default
# 以 powershell -command 执行 Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello
# 以 cmd /S /C 执行 echo hello
SHELL ["cmd", "/S", "/C"]
RUN echo hello
当在Dockerfile中使用它们的shell形式时,RUN
,CMD
和ENTRYPOINT
可能会被SHELL
指令影响,示例:
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
是在Windows上找到的常见模式,可以使用SHELL
指令简化,docker调用的命令将是:
cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
由于调用一个不必要的cmd.exe命令处理器(也就是shell)并且shell形式的每个RUN
指令都需要额外的powershell -command
前缀命令,所以这种方式效率很低。为了提高效率,可以采用两种机制的其中之一,一种是使用RUN
命令的JSON
形式,例如:
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
虽然JSON格式是明确的且不使用不必要的cmd.exe,但它需要通过双引号和转义显得更加冗余。替代机制是使用SHELL
指令和shell形式,为Windows用户提供更自然的语法,特别是与escape
解析器指令结合使用时:
# escape=`
FROM microsoft/nanoserver
SHELL ["powershell","-command"]
RUN New-Item -ItemType Directory C:\Example
ADD Execute-MyCmdlet.ps1 c:\example\
RUN c:\example\Execute-MyCmdlet -sample 'hello world'
SHELL
指令也可用于修改shell的运行方式,例如在Windows上使用SHELL cmd /S /C /V:ON|OFF
,可以修改延迟的环境变量扩展语义。如果需要例如zsh
、csh
、tcsh
等备用shell,也可以在Linux上使用SHELL
指令。
SHELL
指令是在Docker 1.12时添加的。
RUN
指令有两种形式:
RUN
(shell形式,命令在shell中运行,Linux上默认为/bin/sh -c
,Windows上默认为cmd /S /C
)RUN ["executable", "param1", "param2"]
(exec形式)RUN
指令将在当前镜像之上的新的一层执行任何命令并提交结果为一个新镜像,这个新镜像将被Dockerfile
的下一条指令使用。
exec形式可以避免使用shell字符串,并使用不包含指定的shell可执行文件的基础镜像来运行RUN
命令。
exec形式会被解析为JSON数组,因此必须使用"
来包围单词而不是’
。
exec形式不会调用命令shell,也就不会发生正常的shell处理,例如RUN [ "echo", "$HOME" ]
不会对$HOME
进行变量替换。如果需要shell处理,可以使用shell形式或者直接执行shell,例如RUN [ "sh", "-c", "echo $HOME" ]
。当使用exec形式并直接执行shell时,跟shell形式的情况一样是执行环境变量扩展的shell而不是docker。
可以使用SHELL
命令更改shell形式的默认shell。
在shell形式中可以使用\
将单个RUN
指令延续到下一行。以下示例相当于RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
这一行。
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'
想使用除“/bin/sh”
之外的其他shell,需使用传入所需shell的exec形式。例如RUN ["/bin/bash", "-c", "echo hello"]
。
在JSON格式中必须转义反斜杠,在以反斜杠为路径分隔符的Windows上尤其重要。例如RUN ["c:\windows\system32\tasklist.exe"]
由于不是有效的JSON而被视为shell形式,并以一种意想不到的方式失败,而此例正确的语法应为RUN ["c:\\windows\\system32\\tasklist.exe"]
。
RUN
指令的缓存在下一次构建期间不会自动失效。像RUN apt-get dist-upgrade -y
这样的指令的缓存将在下一次构建期间被重新使用。可以使用--no-cache
来使RUN
指令的缓存无效,例如docker build --no-cache
。
RUN
指令分层和生成提交符合Docker的核心概念,并且可以从镜像历史中的任何点创建容器,就像源代码控制一样,因此不必将所有的命令写在一个RUN
指令中。
ADD
指令可以使RUN
指令的缓存无效。
CMD
指令有三种形式:
CMD ["executable","param1","param2"]
(exec形式,首选形式)CMD ["param1","param2"]
(作为ENTRYPOINT的默认参数)CMD command param1 param2
(shell形式)CMD
指令的主要目的是为正在执行的容器提供默认值,些默认值可以包括一个可执行文件,也可以省略可执行文件,在这种情况下必须指定一个ENTRYPOINT
指令。简单来说CMD
指令就是用来给容器在启动时提供默认值,该值可以是一个命令,也可以是一个参数。
Dockerfile
中只能有一条CMD
指令。如果有多个CMD
,则只有最后一个CMD
才会生效。
如果使用CMD
为ENTRYPOINT
指令提供默认参数,则CMD
和ENTRYPOINT
指令都应被指定为JSON数组格式。
exec形式会被解析为JSON数组,因此必须使用"
来包围单词而不是’
。
exec形式不会调用命令shell,也就不会发生正常的shell处理。这跟RUN
和ENTRYPOINT
指令的exec形式一样。
在shell或exec形式中使用时,CMD
指令指定容器启动时默认执行的命令。
如果使用CMD
的shell形式,那么
将在/bin/sh -c
中执行。如果要在没有shell的情况下运行
,则必须将该命令表示为JSON数组并提供可执行文件的完整路径。此数组形式是CMD
的首选格式。任何其他参数必须在数组中单独表示为字符串。
如果用户在启动容器时指定了docker run
的参数,CMD
指定的默认命令将被覆盖。
如果希望容器每次都运行相同的可执行文件,那么建议ENTRYPOINT
与CMD
结合使用。
注意不要将RUN
与CMD
混淆。 RUN
实际上运行一个命令并提交结果为一个新镜像,而CMD
在构建镜像时不执行任何命令,但指定了镜像的预期命令,即在容器启动时默认执行的命令。
ADD
指令有两种形式:
ADD [--chown=:] ...
ADD [--chown=:] ["",... ""]
(包含空格的路径必须使用这种格式)ADD
指令从
复制新文件,目录或URL指向的远程文件并将它们添加到镜像的文件系统
路径中。可以指定多个
资源,但是如果它们是文件或目录,则它们的路径将被解释为相对于构建上下文(context)(上下文就是docker build
命令中指定位置的PATH
或URL
处的文件集合,简单的说就是PATH
或URL
中的所有内容)的源,简单来说就是相对于上下文路径的相对路径。
--chown=
:指定给定用户名,组名或UID/GID组合以请求添加内容的特定所有权,如果没有指定,所有新创建的文件和目录的UID和GID都是0。
--chown
的一些要点:
--chown
的特性只在用于构建Linux容器的Dockerfiles上被支持,并不适用于Windows容器。由于用户和组所有权的概念不能在Linux和Windows之间转换,因此使用/etc/passwd
和/etc/group
将用户名和组名转换为ID会限制此特性只适用于基于Linux OS的容器。--chown
的格式允许用户名和用户组名的字符串或直接整数UID和GID的任意组合。/etc/passwd
和/etc/group
文件将分别用于执行从名称到整数UID或GID的转换。/etc/passwd
或/etc/group
文件,并且在--chown
中使用了用户名或组名,则构建将在ADD
操作上失败。而使用数字ID则不需要查找,因为数字ID不依赖于容器根文件系统内容。
可以包含通配符,匹配时将使用Go的filepath.Match规则完成。示例如下:
# 添加所有以hom开头的文件
ADD hom* /mydir/
# ?可以替换单字符比如home.txt
ADD hom?.txt /mydir/
是绝对路径或相对于WORKDIR
的路径,源文件将被复制到目标容器中,并且源文件的各种元数据都会保留。
不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。示例如下:
# 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test relativeDir/
# 添加 "test" 到 /absoluteDir/
ADD test /absoluteDir/
当添加包含诸如[
和]
这种特殊字符的文件或目录时,需要按照Golang规则转义这些路径,以防止它们被视为匹配模式。例如要添加名为arr[0].txt
的文件,需使用命令:ADD arr[[]0].txt /mydir/
。
在
是远程文件URL的情况下,目标将具有600的权限。如果正在检索的远程文件具有HTTP的Last-Modified
头,则该头的时间戳将用于在目标文件上设置mtime
。但是与ADD
期间处理的任何其他文件一样,在确定文件是否更改以及是否应该更新缓存时,并不包括mtime
。
如果通过将Dockerfile
传递给STDIN(docker build - < somefile
)来构建,则没有构建上下文(context),因此Dockerfile
只能包含基于URL的ADD
指令。还可以通过STDIN传递压缩存档(docker build - < archive.tar.gz
),存档文件根目录下的Dockerfile
和存档文件的其余部分将用作构建上下文使用。
ADD
指令不支持身份验证,如果URL文件使用了身份验证进行保护,则需要使用RUN wget
,RUN curl
或使用容器内的其他工具。
ADD
指令获取远程URL中的压缩包不是推荐的做法,因为需要额外的一层RUN
指令进行解压缩,应该使用RUN wget
或RUN curl
代替,这样可以删除解压后不再需要的文件,并且不用在镜像中再添加一层。
如果
的内容已更改,则第一个遇到的ADD
指令将使来自Dockerfile的包括RUN
指令在内的所有后续指令的缓存无效。
ADD
遵守以下规则:
路径必须位于构建上下文(context)中,不能ADD ../something/something
,因为docker build
的第一步是将上下文目录(和子目录)发送给docker守护进程。
是URL且
不以斜杠结尾,则从URL下载文件并将其复制到
。
是URL并且
以斜杠结尾,则从URL推断文件名,并将文件下载到/
。例如ADD http://example.com/foobar /
将创建文件/foobar
。URL必须具有一个有意义的路径,以便在这种情况下可以发现适当的文件名(http://example.com
将不起作用)。
是目录,则复制目录包括文件系统元数据在内的全部内容,注意不复制目录本身,只复制其内容。
是可识别的压缩格式(identity,gzip,bzip2或xz)的本地tar存档,则将其解压缩为目录。远程URL中的资源则不解压。当目录被复制或解压缩时,它的行为与tar -x
具相同。注意,文件是否被识别为可识别的压缩格式是完全基于文件的内容而不是文件的名称完成的。例如一个空文件以.tar.gz
结尾,则不会将其识别为压缩文件,并且不会生成任何类型的解压缩错误消息,而是将文件简单复制到目标。
是任何其他类型的文件,它将与其元数据一起单独复制。在这种情况下,如果
以斜杠/
结尾,则将其视为目录,
的内容将写入/base()
。
资源,则
必须是目录并且以斜杠/
结尾。
不以尾部斜杠结束,则它将被视为常规文件,
的内容将写入
。
不存在,则会在其路径中创建所有缺少的目录。COPY
指令有两种形式:
COPY [--chown=:] ...
COPY [--chown=:] ["",... ""]
(包含空格的路径必须使用这种格式)COPY
指令从
复制新文件或目录,并将它们添加到容器的文件系统的
路径中。可以指定多个
资源,但是如果它们是文件或目录,则它们的路径将被解释为相对于构建上下文(context)的源,简单来说就是相对于上下文路径的相对路径。
--chown=
的含义和要点跟ADD
中的一样,见ADD
部分。
可以包含通配符,匹配时将使用Go的filepath.Match规则完成。
是绝对路径或相对于WORKDIR
的路径,源文件将被复制到目标容器中,并且源文件的各种元数据都会保留。
不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
当添加包含诸如[
和]
这种特殊字符的文件或目录时,需要按照Golang规则转义这些路径,以防止它们被视为匹配模式。例如要添加名为arr[0].txt
的文件,需使用命令:COPY arr[[]0].txt /mydir/
。
如果使用STDIN(docker build - < somefile
)进行构建,则没有构建上下文(context),因此不能使用COPY
。
COPY
接受一个可选标志--from=
,可用于将源位置设置为先前的构建阶段(使用FROM .. AS
创建)而不是由用户发送的构建上下文。该标志还接受使用FROM
指令启动的所有先前构建阶段分配的数字索引。如果找不到具有指定名称的构建阶段,则尝试使用具有相同名称的镜像。
COPY
和ADD
指令的功能相似,但ADD
的功能比COPY
更复杂,当使用本地目录或文件为源时,推荐使用COPY
。如果
为一个本地tar压缩文件,ADD
会自动解压这个文件到
,如果只是单纯希望复杂这个压缩文件而不解压缩,这时候只能使用COPY
。推荐仅在需要自动解压缩或从URL指向的远程文件添加的情况下使用ADD
。
COPY
遵守以下规则:
路径必须位于构建上下文(context)中,不能COPY ../something /something
,因为docker build
的第一步是将上下文目录(和子目录)发送给docker守护进程。
是目录,则复制目录包括文件系统元数据在内的全部内容,注意不复制目录本身,只复制其内容。
是任何其他类型的文件,它将与其元数据一起单独复制。在这种情况下,如果
以斜杠/
结尾,则将其视为目录,
的内容将写入/base()
。
资源,则
必须是目录并且以斜杠/
结尾。
不以尾部斜杠结束,则它将被视为常规文件,
的内容将写入
。
不存在,则会在其路径中创建所有缺少的目录。在编写完Dockerfile
之后,就可以使用docker build
命令从Dockerfile
和上下文(context)中构建镜像。其中docker build
命令的语法为:
docker build [OPTIONS] PATH | URL | -
构建的上下文(context)是指定位置的PATH
或URL
处的文件集合,简单的说就是PATH
或URL
中的所有内容。PATH
是本地文件系统上的目录, URL
是Git存储库位置。上下文是递归处理的,因此PATH
包括任何子目录,URL
包括存储库及其子模块。
docker build
命令支持的可选项如下:
--add-host 添加自定义主机名到IP的映射(host:ip)
--build-arg 设置镜像创建时的变量
--cache-from 使用指定镜像作为缓存源
--cgroup-parent 继承自上层的cgroup
--compress 构建上下文时使用gzip压缩
--cpu-period 限制CPU CFS(Completely Fair Scheduler)周期
--cpu-quota 限制CPU CFS(Completely Fair Scheduler)配额
--cpu-shares,-c CPU的使用权重
--cpuset-cpus 指定使用的cpu id
--cpuset-mems 指定使用的内存 id
--disable-content-trust 跳过镜像验证,默认值为true
--file,-f 指定要使用的Dockerfile的名称(默认为‘PATH/Dockerfile’)
--force-rm 总是删除中间容器
--iidfile 将镜像id写进一个文件
--isolation 使用容器隔离技术
--label 设置镜像元数据
--memory,-m 最大内存
--memory-swap 设置Swap的最大值等于内存加swap,'-1'表示不限制swap
--network 在构建期间设置RUN指令的网络模式
--no-cache 构建镜像过程中不使用缓存
--output,-o 输出到的目的地(format: type=local,dest=path)
--platform 如果server支持多平台可以设置平台
--progress 设置进度输出类型(auto,plain,tty),默认值为auto
--pull 总是尝试拉取一个新版本镜像
--quiet,-q 压缩构建输出并且成功只打印镜像id
--rm 构建镜像成功后删除中间容器,默认值为true
--secret 私密文件公开构建(仅当启用了BuildKit时):id=mysecret,src=/local/secret
--security-opt 安全设置
--shm-size 设置/dev/shm的大小
--squash 将新创建的多层挤压放入到一层中
--ssh SSH代理套接字或密钥公开构建(仅当启用了BuildKit时)(格式:default|[=|[,]])
--stream 以流形式持续获取创建的上下文
--tag,-t 镜像的名字及标签,通常为'name:tag'格式
--target 设置创建的目标阶段
--ulimit Ulimit配置
当Docker客户端接收到用户命令,首先解析命令行参数。根据第一个参数的不同将分为以下4种情况分别处理:
情况1,第一个参数为“-
”,例如:
#从STDIN中读入Dockerfile,没有context
docker build - < Dockerfile
#或从STDIN中读入压缩的context
docker build - <context.tar.gz
此时根据命令行输入参数对Dockerfile
和context进行设置。
情况2,第一个参数为Git远程仓库的URL,例如:
docker build github.com/creack/docker-firefox
则调用git clone--depth 1--recursive
命令克隆该Git远程仓库,该操作会在本地的一个临时目录中进行,命令成功之后该目录将作为context传给Docker守护进程(daemon),该目录中的Dockerfile
之后会被用来进行镜像构建。
情况3,第一个参数为URL,但不是Git远程仓库的URL,然后从该URL下载context,并将其封装为一个名为io.Reader的io流,后面的处理与情况1相同,只是将STDIN换为了io.Reader。
情况4,context为本地文件或目录:
#使用当前目录作为context
docker build -t vieux/apache:2.0 .
#或使用/home/me/myapp/dockerfiles/debug作为Dockerfile /home/me/myapp作为context
docker build -f /home/me/myapp/dockerfiles/debug /home/me/myapp
如果上下文(context)中有.dockerignore
文件,则将上下文中文件名满足其定义的规则的文件都从上传列表去除,不打包上传给Docker守护进程(daemon),如果用户定义了tag,则对其指定的repository和tag进行验证。完成了相关信息的设置后Docker客户端会向Docker服务端发送POST/build
的HTTP请求,包含了所需的上下文。
由于构建过程的第一件事是将整个上下文(context)递归地发送到Docker守护进程(daemon),如果上下文过大,会导致发送大量数据给服务端,延缓创建过程,因此除非是生成镜像所必需的文件,不然不要放到上下文路径下,最好将空目录作为上下文,并将Dockerfile
保存在该目录中,仅添加构建Dockerfile
所需的文件。默认情况下,如果不额外指定Dockerfile
,会将上下文目录下的名为Dockerfile
的文件作为Dockerfile
,如果使用非上下文路径下的Dockerfile
,可以通过-f
来指定其路径,不过Dockerfile
文件一般习惯使用默认的文件名Dockerfile
并将其置于上下文目录。
警告:不要将根目录
/
用作PATH
,因为它会导致构建将硬盘驱动器的全部内容传输到Docker守护进程(daemon)。
Docker服务端接收到Docker客户端发来的HTTP请求之后,会进行以下步骤构建镜像,构建由Docker守护进程(daemon)运行。
Dockerfile
文件,执行Dockerfile
的初步验证,如果语法不正确会返回错误。Dockerfile
中的指令,每条指令都是独立运行的,并且会导致创建新镜像。除了FROM
指令,其他每一条指令都会在上一条指令所生成镜像的基础上执行,执行完成提交生成一个新的镜像层,然后将新镜像层覆盖到原镜像之上形成一个新镜像。Dockerfile
所生成的最终镜像就是在基础镜像上面叠加一层层的镜像层构建的,如果指定了tag,就给镜像打上tag,最后一次提交生成的镜像ID会作为最终的镜像ID返回。为了加速镜像构建,Docker守护进程(daemon)会缓存构建过程中的中间镜像。当从一个已在缓存中的基础镜像开始构建新镜像时,会将Dockerfile
中的下一条指令和基础镜像的所有子镜像比较,如果有一个子镜像是由相同的指令生成的,则命中缓存,直接使用该镜像,而不用再生成一个新的镜像。在寻找缓存的过程中,COPY
和ADD
指令与其他指令稍有不同,其他指令只对比生成镜像的指令字符串是否相同,ADD
和COPY
指令除了对比指令字符串外还要对比容器中的文件内容与ADD
和COPY
所添加的文件内容是否相同。此外,在镜像构建过程中缓存一旦失效,则后续的指令都将生成新的镜像,而不再使用缓存。所以为了有效利用缓存,需要保证指令的连续性,尽量将所有Dockerfile
文件中相同的部分都放在前面,而将不同的部分放在后面,达到最大化复用。
在docker CLI将上下文(context)发送到Docker守护进程(daemon)之前,它会在上下文的根目录中查找名为.dockerignore
的文件。如果此文件存在,CLI将修改上下文以排除与其中的模式匹配的文件和目录。这有助于避免不必要地将大型或敏感的文件和目录发送到守护进程,并可能使用ADD
或COPY
将它们添加到镜像。
.dockerignore
文件被CLI解释为用换行符分隔的模式列表,类似Unix shell的文件globs。为了匹配,上下文(context)的根目录被认为是工作目录和根目录。例如模式/foo/bar
和foo/bar
都会在PATH
的foo
子目录中或位于URL的git远程仓库的根目录中排除名为bar
的文件或目录,两者都不会排除任何其他内容。如果.dockerignore
文件中的一行的第一列以#
开头,则此行被视为注释,并在CLI解释之前被忽略。
.dockerignore
文件中的模式匹配基于Go的filepath.Match规则完成:
#
:视为注释,将被忽略。*
:表示任意多个字符,例如*/*/temp*
将从根目录下两级的任何子目录中排除以temp开头的文件和目录。**
:表示任意数量的目录(包括零),例如**/*.go
将排除包括构建上下文(context)的根目录在内的所有目录中以.go
结尾的所有文件。?
:表示单个字符,例如temp?
将排除根目录中名称是temp的单字符扩展的文件和目录。!
:表示不匹配(不排除指定的文件或目录),例如!README.md
将不会排除README.md
。!
所处位置会影响行为是一个例外规则,.dockerignore
的最后一行指定的文件决定了它是否应该包含或者排除,例如:
*.md
!README*.md
README-secret.md
除了README-secret.md
文件以外的以README
开头的Markdown文件都不会被排除,其它的Markdown文件都会被排除。
*.md
README-secret.md
!README*.md
包含所有README文件(除了所有README
开头的文件外其他Markdown文件都会被排除),中间行没有效果,因为最后一行!README*.md
与README-secret.md
匹配。
甚至可以使用.dockerignore
文件来排除Dockerfile
和.dockerignore
文件。这些文件仍然会被发送到Docker守护进程(daemon),但ADD
和COPY
指令不会将它们复制到镜像中。
如果希望指定要包含在上下文(context)中的文件而不是要排除的文件。可以将*
指定为第一个模式,然后指定一个或多个模式!
模式。
下面通过编写一个简单的Dockerfile
来整体感受一下构建镜像的过程。当然在进行本示例之前需安装Docker,安装过程这里就不再赘述了。本示例所使用的环境是CentOS Linux release 7.7.1908
系统和19.03.5
版本的Docker。这里基于ubuntu:20.10
构建一个jdk1.8环境的镜像。首先拉取ubuntu:20.10
镜像,再创建一个目录/usr/local/jdk1.8_ubuntu/
作为上下文目录,然后上传jdk-8u212-linux-x64.tar.gz
至/usr/local/jdk1.8_ubuntu/
目录。然后编写Dockerfile
文件,内容如下:
# 指定基础镜像
FROM ubuntu:20.10
# 设置元数据信息
LABEL maintainer="RtxtitanV" version="1.0.0" description="JDK1.8 IMAGE"
# 指定工作目录
WORKDIR /usr/local/work
# 将jdk压缩包添加并解压至工作目录下的java目录下
ADD jdk-8u212-linux-x64.tar.gz java/
# 设置jdk环境变量
ENV JAVA_HOME /usr/local/work/java/jdk1.8.0_212
ENV PATH $JAVA_HOME/bin:$PATH
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$CLASSPATH
然后将Dockerfile
文件放入/usr/local/jdk1.8_ubuntu/
目录,命令cd /usr/local/jdk1.8_ubuntu/
进入该目录下,执行以下命令构建镜像:
docker build -t jdk1.8/ubuntu:v1.0.0 .
执行过程如下:
Sending build context to Docker daemon 195MB
Step 1/7 : FROM ubuntu:20.10
---> bafda420a37f
Step 2/7 : LABEL maintainer="RtxtitanV" version="1.0.0" description="JDK1.8 IMAGE"
---> Running in c7039610faa4
Removing intermediate container c7039610faa4
---> 372cfee4b3d6
Step 3/7 : WORKDIR /usr/local/work
---> Running in 3e566923dab8
Removing intermediate container 3e566923dab8
---> 01eab14c3aa2
Step 4/7 : ADD jdk-8u212-linux-x64.tar.gz java/
---> 4c94774db8d4
Step 5/7 : ENV JAVA_HOME /usr/local/work/java/jdk1.8.0_212
---> Running in a657e430c6dc
Removing intermediate container a657e430c6dc
---> 38efa7747c8f
Step 6/7 : ENV PATH $JAVA_HOME/bin:$PATH
---> Running in a22409dad523
Removing intermediate container a22409dad523
---> c490f7279050
Step 7/7 : ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$CLASSPATH
---> Running in 60b76ca90bf1
Removing intermediate container 60b76ca90bf1
---> b617770c39ee
Successfully built b617770c39ee
Successfully tagged jdk1.8/ubuntu:v1.0.0
构建成功,查看新构建的镜像:
[root@iZwz94v2sdd3v6zcczsu67Z jdk1.8_ubuntu]# docker images jdk1.8/ubuntu:v1.0.0
REPOSITORY TAG IMAGE ID CREATED SIZE
jdk1.8/ubuntu v1.0.0 b617770c39ee 2 minutes ago 479MB
使用新构建的jdk1.8/ubuntu:v1.0.0
镜像运行一个容器并验证JDK环境,结果如下,输出了JDK版本信息说明JDK环境没问题。
[root@iZwz94v2sdd3v6zcczsu67Z jdk1.8_ubuntu]# docker run -it --name myjdk1.8 jdk1.8/ubuntu:v1.0.0 bash
root@285adebd9438:/usr/local/work# java -version
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode)
root@285adebd9438:/usr/local/work#