通过Dockerfile构建Docker镜像

Dockerfile是Docker用来构建镜像的文本文件,包含自定义的指令和格式。可以通过docker build命令利用Dockerfile构建镜像。Dockerfile提供了一系列统一的资源配置语法指令,开发人员可以根据需求定制Dockerfile,然后使用这份Dockerfile文件进行自动化镜像构建,简化了构建镜像的复杂过程,同时Dockerfile与镜像配合使用,使Docker在构建时可以充分利用镜像的功能进行缓存,大大提升了Docker的使用效率。

本文主要对Dockerfile指令和使用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格式

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'

三、解析器指令(Parser directives)

解析器指令是可选的,并且会影响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

解析器指令支持syntaxescape

1.syntax

格式

# 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实现能够:

  • 自动获取错误修正而无需更新守护程序
  • 确保所有用户都使用相同的实现来构建Dockerfile
  • 使用最新功能而不更新守护程序
  • 试用新的测试性的或第三方的功能
正式发布(Official releases)

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,如果想从测试功能中受益,则应使用测试版渠道,如果正在使用测试版渠道,则较新的版本可能无法向后兼容,因此建议使用不可变的完整版本。

2.escape

格式

# 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:\

四、环境替换(Environment replacement)

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中支持环境变量的有以下指令:ADDCOPYENVEXPOSEFROMLABELSTOPSIGNALUSERVOLUMEWORKDIRONBUILD
注意:环境变量替换将在整个指令中为每个变量使用相同的值。例如:

ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

def的值为hello不是bye,然而ghi的值为bye,这是因为它不是将abc设置为bye的相同指令的一部分。

五、构建指令

Dockerfile的格式上文已经总结过了,其中用于构建镜像的构建指令一般格式为INSTRUCTION arguments,而Dockerfile中一般有以下构建指令:FROMLABELMAINTAINEREXPOSEENVENTRYPOINTVOLUMEUSERWORKDIRARGONBUILDSTOPSIGNALHEALTHCHECKSHELLRUNCMDADDCOPY。下面将对这些指令进行简单总结。

1.FROM

格式

FROM [--platform=] [AS ]

FROM [--platform=] [:] [AS ]

FROM [--platform=] [@] [AS ]

功能描述

FROM指令初始化一个新的构建阶段并为后续指令设置基础镜像。因此有效的Dockerfile必须以FROM指令开头,其中基础镜像可以是任何有效镜像,而通过从公有仓库中拉取镜像来启动它尤其容易。

选项

--platform选项在FROM引用了多平台镜像的情况下可以用于指定镜像所属的平台。例如linux/amd64linux/arm64windows/amd64,默认会使用发起构建请求的目标平台,也可以在该选项的值中使用全局构建参数。

注意

ARGDockerfile中唯一可以在FROM之前的指令。

FROM可以在单个Dockerfile中多次出现以创建多个镜像,或者使用一个构建阶段作为另一个构建阶段的依赖项。这只需在每个新的FROM指令之前记下提交输出的最后一个镜像ID。每个FROM指令会清除先前指令创建的任何状态。

通过将AS name添加到FROM指令可以将可选的名称赋予到新的构建阶段,该名称可以在后续的FROMCOPY --from=指令中使用,以引用此阶段构建的镜像。

tagdigest的值是可选的,如果省略其中任何一个,构建器默认采用latest标签,如果找不到tag值,构建器将返回错误。

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

FROM之前声明的ARG在构建阶段之外,因此在FROM之后的任何指令中都不能使用它。要使用在第一个FROM之前声明的ARG的默认值,需在构建阶段内使用没有值的ARG指令。示例如下:

ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version

2.LABEL

格式

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

3.MAINTAINER[已弃用(deprecated)]

格式

MAINTAINER

功能描述

MAINTAINER指令设置生成镜像的作者(Author)字段。不过LABEL指令使用起来比MAINTAINER更灵活,可以设置我们所需的任何元数据并且能够轻松查看这些元数据,所以比起MAINTAINER指令更推荐使用LABEL指令。要设置与MAINTAINER字段对应的label,可以使用下面示例中的LABEL指令:

LABEL maintainer="[email protected]"

MAINTAINER字段可以通过docker inspect与其他label一起显示出来。

4.EXPOSE

格式

EXPOSE [/...]

功能描述

EXPOSE指令通知Docker容器在运行时监听指定的网络端口,可以指定端口是监听TCP还是UDP,如果未指定网络协议,默认监听TCP。

注意

EXPOSE指令只是起到声明作用,并不会自动完成端口映射,也就是实际上不会发布端口。

如果想要在运行容器时实际发布端口,即完成端口映射,需要在docker run即创建运行容器时使用-p具体指定一个或多个主机端口与容器端口间的映射,或者使用-P,对容器中被监听的每个端口,Docker都将自动分配一个主机临时端口与之完成映射。

默认情况EXPOSE指定端口是监听TCP,也可以指定为UDP,例如markup EXPOSE 80/udp。如果要指定端口是同时监听TCP和UDP,需要包括两行指令,下面是一个示例,在这种情况如果将-Pdocker 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 ...

5.ENV

格式

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 =

6.ENTRYPOINT

格式

ENTRYPOINT指令有两种形式:

  • ENTRYPOINT ["executable", "param1", "param2"](exec形式,推荐形式)
  • ENTRYPOINT command param1 param2(shell形式)
功能描述

ENTRYPOINT指令指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有传入值作为该命令的参数。

注意

使用exec形式时docker run 传入的命令行参数会被附加到ENTRYPOINT所有元素之后,并将覆盖CMD指定的所有元素。这允许将参数传递给入口点,即docker run -d-d参数传递给入口点。使用docker run --entrypoint可以覆盖ENTRYPOINT指令。

使用shell形式时ENTRYPOINT指令会防止任何CMDrun命令行参数被使用,但缺点是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间的相互作用

CMDENTRYPOINT指令都定义了运行容器时执行的命令,以下为它们之间协作的规则。

  1. Dockerfile应至少指定一个CMDENTRYPOINT命令。
  2. 使用容器作为可执行文件时应定义ENTRYPOINT
  3. CMD应该作为ENTRYPOINT命令定义默认参数或在容器中执行特定命令的方法。
  4. 使用备用参数运行容器时CMD将被覆盖。

下表展示了不同的ENTRYPOINTCMD组合执行的命令:

. 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

7.VOLUME

格式

VOLUME ["/data"]

功能描述

VOLUME指令创建一个具有指定名称的挂载点,并将其标记为从本机主机或其他容器保存的外部挂载卷,该值可以是JSON数组或具有多个参数的普通字符串。使用docker run命令会用任何存在于基础镜像内指定位置的数据初始化新创建的卷。

注意

以下是关于Dockerfile中的卷的注意事项:

基于Windows容器的卷:使用基于Windows的容器时,容器中卷的目标必须是一个不存在的或空目录和C:以外的驱动器这两者之一。

从Dockerfile中更改卷:如果任何构建步骤在声明后更改卷内的数据,那么这些更改将被丢弃。

JSON格式:将列表解析为JSON数组必须使用"而不是'

主机目录在容器运行时被声明:主机目录(挂载点)本质上是依赖于主机的。这是为了保持镜像的可移植性,因为不能保证给定的主机目录在所有主机上都可用,因此无法从Dockerfile中挂载主机目录。VOLUME指令不支持指定host-dir参数,在创建或运行容器时必须指定挂载点。

8.USER

格式

USER [:]

USER [:]

功能描述

USER指令设置用户名或UID以及可选的用户组或GID,当运行镜像以及在Dockerfile中紧接着的所有RUNCMDENTRYPOINT指令时会使用该指定用户。

注意

当为用户指定用户组时,用户将只有指定的用户组的成员身份,任何其他已配置的用户组的成员身份将被忽略。

当用户没有主用户组时将使用root组运行镜像或下一条指令。

在Windows上,如果用户不是内置帐户,则必须先创建用户,这可以通过作为Dockerfile一部分调用的net user命令来完成。示例如下:

FROM microsoft/windowsservercore
# 在容器中创建Windows用户
RUN net user /add patrick
# 为后续命令设置用户名
USER patrick

9.WORKDIR

格式

WORKDIR /path/to/workdir

功能描述

WORKDIR指令为Dockerfile中的任何RUNCMDENTRYPOINTCOPYADD指令设置工作目录。如果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

10.ARG

格式

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

我们可以使用ARGENV指令指定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
预定义ARG变量

Docker有以下一组在Dockerfile中使用而无需相应的ARG指令的预定义的ARG变量:HTTP_PROXYhttp_proxyHTTPS_PROXYhttps_proxyFTP_PROXYftp_proxyNO_PROXYno_proxy。要使用这些预定义ARG变量,只需通过--build-arg =在命令行上传递它们。默认情况下,这些预定义变量将从docker history的输出中排除,排除它们可降低在HTTP_PROXY变量中意外泄露敏感验证信息的风险。

11.ONBUILD

格式

ONBUILD

功能描述

ONBUILD指令向镜像添加将在稍后执行的触发器指令,当该镜像作为另一个镜像构建的基础时,触发器将在下游构建的上下文中执行,就好像是在下游Dockerfile中的FROM指令之后立即插入一样,换句话说,就是ONBUILD指令向镜像添加了触发器指令,当该镜像(父镜像)作为构建另一个镜像(子镜像)的基础镜像时,在构建另一个镜像(子镜像)时,这些触发器指令就在构建另一个镜像(子镜像)的DockerfileFROM指令执行时加入到构建过程,看起来就像是触发器指令在FROM指令之后执行。

注意

任何构建指令都可以注册为触发器,但是不允许使用ONBUILD ONBUILD这样的链状ONBUILD指令,即ONBUILD指令中不能包含ONBUILD指令,并且ONBUILD指令可能不会触发FROMMAINTAINER指令。当需要制作一个用来构建其他镜像的基础镜像时,ONBUILD指令会很有用。

ONBUILD指令的工作原理
  1. 当遇到ONBUILD指令时构建器会将触发器添加到正在构建的镜像的元数据中,该指令不会影响当前构建,即不会在当前构建流程中执行。
  2. 在构建结束时,所有触发器的列表都存储在镜像清单中的OnBuild键下,可以使用docker inspect查看。
  3. 之后可以使用FROM指令将该镜像作为新构建镜像的基础镜像。作为处理FROM指令的一部分,下游构建器查找ONBUILD触发器,并按照它们注册的顺序执行它们。如果任何触发器指令执行失败,FROM指令会被终止,导致构建失败。如果所有触发器指令执行成功,FROM指令成功执行完毕并且构建会继续正常进行。
  4. 在镜像构建完成之后,触发器指令会从最终镜像中清除,换句话说,它们不会被子孙镜像继承。

12.STOPSIGNAL

格式

STOPSIGNAL signal

功能描述

STOPSIGNAL指令设置将发送给容器退出的系统调用信号。此信号可以是与内核的系统调用表中的位置匹配的有效无符号数,例如9或像SIGKILL这样的SIGNAME格式的信号名。

13.HEALTHCHECK

格式

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

命令的退出状态指示容器的运行状况,可能的值有:

  • 0:成功 - 容器健康可以使用
  • 1:不健康 - 容器不能正常工作
  • 2:保留 - 不要使用此退出码

为了帮助调试错误,命令在stdout或stderr上写入的任何输出文本(UTF-8编码)都将存储在健康状态中并可以使用docker inspect进行查询,此类输出应该保持简短(目前仅存储前4096个字节)。

当容器的健康状态改变时将使用新的状态生成health_status事件。

HEALTHCHECK指令是在Docker 1.12时添加的。

14.SHELL

格式

SHELL ["executable", "parameters"]

功能描述

SHELL指令允许用于命令的shell形式的默认shell被覆盖。Linux上的默认shell是["/bin/sh", "-c"],Windows上是["cmd", "/S", "/C"]。SHELL指令必须以JSON格式写入Dockerfile。

注意

SHELL指令在Windows上特别有用,其中有cmdpowershell这两个常用且相当不同的本机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形式时,RUNCMDENTRYPOINT可能会被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,可以修改延迟的环境变量扩展语义。如果需要例如zshcshtcsh等备用shell,也可以在Linux上使用SHELL指令。

SHELL指令是在Docker 1.12时添加的。

15.RUN

格式

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指令的缓存无效。

16.CMD

格式

CMD指令有三种形式:

  • CMD ["executable","param1","param2"](exec形式,首选形式)
  • CMD ["param1","param2"](作为ENTRYPOINT的默认参数)
  • CMD command param1 param2(shell形式)
功能描述

CMD指令的主要目的是为正在执行的容器提供默认值,些默认值可以包括一个可执行文件,也可以省略可执行文件,在这种情况下必须指定一个ENTRYPOINT指令。简单来说CMD指令就是用来给容器在启动时提供默认值,该值可以是一个命令,也可以是一个参数。

注意

Dockerfile中只能有一条CMD指令。如果有多个CMD,则只有最后一个CMD才会生效。

如果使用CMDENTRYPOINT指令提供默认参数,则CMDENTRYPOINT指令都应被指定为JSON数组格式。

exec形式会被解析为JSON数组,因此必须使用"来包围单词而不是

exec形式不会调用命令shell,也就不会发生正常的shell处理。这跟RUNENTRYPOINT指令的exec形式一样。

在shell或exec形式中使用时,CMD指令指定容器启动时默认执行的命令。

如果使用CMD的shell形式,那么将在/bin/sh -c中执行。如果要在没有shell的情况下运行,则必须将该命令表示为JSON数组并提供可执行文件的完整路径。此数组形式是CMD的首选格式。任何其他参数必须在数组中单独表示为字符串。

如果用户在启动容器时指定了docker run的参数,CMD指定的默认命令将被覆盖。

如果希望容器每次都运行相同的可执行文件,那么建议ENTRYPOINTCMD结合使用。

注意不要将RUNCMD混淆。 RUN实际上运行一个命令并提交结果为一个新镜像,而CMD在构建镜像时不执行任何命令,但指定了镜像的预期命令,即在容器启动时默认执行的命令。

17.ADD

格式

ADD指令有两种形式:

  • ADD [--chown=:] ...
  • ADD [--chown=:] ["",... ""](包含空格的路径必须使用这种格式)
功能描述

ADD指令从复制新文件,目录或URL指向的远程文件并将它们添加到镜像的文件系统路径中。可以指定多个资源,但是如果它们是文件或目录,则它们的路径将被解释为相对于构建上下文(context)(上下文就是docker build命令中指定位置的PATHURL处的文件集合,简单的说就是PATHURL中的所有内容)的源,简单来说就是相对于上下文路径的相对路径。

选项

--chown=::指定给定用户名,组名或UID/GID组合以请求添加内容的特定所有权,如果没有指定,所有新创建的文件和目录的UID和GID都是0。

--chown的一些要点:

  • --chown的特性只在用于构建Linux容器的Dockerfiles上被支持,并不适用于Windows容器。由于用户和组所有权的概念不能在Linux和Windows之间转换,因此使用/etc/passwd/etc/group将用户名和组名转换为ID会限制此特性只适用于基于Linux OS的容器。
  • --chown的格式允许用户名和用户组名的字符串或直接整数UID和GID的任意组合。
  • 提供没有用户组名的用户名或没有GID的UID将使用与GID相同的数字UID。
  • 如果提供了用户名或组名,则容器的根文件系统/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 wgetRUN curl或使用容器内的其他工具。

ADD指令获取远程URL中的压缩包不是推荐的做法,因为需要额外的一层RUN指令进行解压缩,应该使用RUN wgetRUN curl代替,这样可以删除解压后不再需要的文件,并且不用在镜像中再添加一层。

如果的内容已更改,则第一个遇到的ADD指令将使来自Dockerfile的包括RUN指令在内的所有后续指令的缓存无效。

ADD遵守的规则

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()
  • 如果直接或由于使用通配符指定了多个资源,则必须是目录并且以斜杠/结尾。
  • 如果不以尾部斜杠结束,则它将被视为常规文件,的内容将写入
  • 如果不存在,则会在其路径中创建所有缺少的目录。

18.COPY

格式

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指令启动的所有先前构建阶段分配的数字索引。如果找不到具有指定名称的构建阶段,则尝试使用具有相同名称的镜像。

COPYADD指令的功能相似,但ADD的功能比COPY更复杂,当使用本地目录或文件为源时,推荐使用COPY。如果为一个本地tar压缩文件,ADD会自动解压这个文件到,如果只是单纯希望复杂这个压缩文件而不解压缩,这时候只能使用COPY。推荐仅在需要自动解压缩或从URL指向的远程文件添加的情况下使用ADD

COPY遵守的规则

COPY遵守以下规则:

  • 路径必须位于构建上下文(context)中,不能COPY ../something /something,因为docker build的第一步是将上下文目录(和子目录)发送给docker守护进程。
  • 如果是目录,则复制目录包括文件系统元数据在内的全部内容,注意不复制目录本身,只复制其内容。
  • 如果是任何其他类型的文件,它将与其元数据一起单独复制。在这种情况下,如果以斜杠/结尾,则将其视为目录,的内容将写入/base()
  • 如果直接或由于使用通配符指定了多个资源,则必须是目录并且以斜杠/结尾。
  • 如果不以尾部斜杠结束,则它将被视为常规文件,的内容将写入
  • 如果不存在,则会在其路径中创建所有缺少的目录。

六、构建镜像

在编写完Dockerfile之后,就可以使用docker build命令从Dockerfile和上下文(context)中构建镜像。其中docker build命令的语法为:

docker build [OPTIONS] PATH | URL | -

构建的上下文(context)是指定位置的PATHURL处的文件集合,简单的说就是PATHURL中的所有内容。PATH是本地文件系统上的目录, URL是Git存储库位置。上下文是递归处理的,因此PATH包括任何子目录,URL包括存储库及其子模块。

1.命令选项

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配置

2.docker build命令构建镜像过程

Docker客户端

当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服务端接收到Docker客户端发来的HTTP请求之后,会进行以下步骤构建镜像,构建由Docker守护进程(daemon)运行。

  1. 创建一个临时目录并将上下文(context)指定的文件系统解压到该目录下。
  2. 读取并解析Dockerfile文件,执行Dockerfile的初步验证,如果语法不正确会返回错误。
  3. 逐个运行Dockerfile中的指令,每条指令都是独立运行的,并且会导致创建新镜像。除了FROM指令,其他每一条指令都会在上一条指令所生成镜像的基础上执行,执行完成提交生成一个新的镜像层,然后将新镜像层覆盖到原镜像之上形成一个新镜像。
  4. Dockerfile所生成的最终镜像就是在基础镜像上面叠加一层层的镜像层构建的,如果指定了tag,就给镜像打上tag,最后一次提交生成的镜像ID会作为最终的镜像ID返回。

为了加速镜像构建,Docker守护进程(daemon)会缓存构建过程中的中间镜像。当从一个已在缓存中的基础镜像开始构建新镜像时,会将Dockerfile中的下一条指令和基础镜像的所有子镜像比较,如果有一个子镜像是由相同的指令生成的,则命中缓存,直接使用该镜像,而不用再生成一个新的镜像。在寻找缓存的过程中,COPYADD指令与其他指令稍有不同,其他指令只对比生成镜像的指令字符串是否相同,ADDCOPY指令除了对比指令字符串外还要对比容器中的文件内容与ADDCOPY所添加的文件内容是否相同。此外,在镜像构建过程中缓存一旦失效,则后续的指令都将生成新的镜像,而不再使用缓存。所以为了有效利用缓存,需要保证指令的连续性,尽量将所有Dockerfile文件中相同的部分都放在前面,而将不同的部分放在后面,达到最大化复用。

3.使用.dockerignore文件

在docker CLI将上下文(context)发送到Docker守护进程(daemon)之前,它会在上下文的根目录中查找名为.dockerignore的文件。如果此文件存在,CLI将修改上下文以排除与其中的模式匹配的文件和目录。这有助于避免不必要地将大型或敏感的文件和目录发送到守护进程,并可能使用ADDCOPY将它们添加到镜像。
.dockerignore文件被CLI解释为用换行符分隔的模式列表,类似Unix shell的文件globs。为了匹配,上下文(context)的根目录被认为是工作目录和根目录。例如模式/foo/barfoo/bar都会在PATHfoo子目录中或位于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*.mdREADME-secret.md匹配。

甚至可以使用.dockerignore文件来排除Dockerfile.dockerignore文件。这些文件仍然会被发送到Docker守护进程(daemon),但ADDCOPY指令不会将它们复制到镜像中。

如果希望指定要包含在上下文(context)中的文件而不是要排除的文件。可以将*指定为第一个模式,然后指定一个或多个模式!模式。

七、Dockerfile构建镜像示例

下面通过编写一个简单的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#

你可能感兴趣的:(Docker,docker,容器,linux,centos,运维)