1. Docker镜像原理

1.1 镜像是什么?

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需要的所有的内容,包括代码、运行时、库、环境变量和配置文件。

1.2 UnionFS(联合文件系统)

UnionFS(联合文件系统):Union文件系统是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

特征:一次同时加载多个文件系统,但从外面看起爱,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。

1.3 Docker 镜像加载原理

docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统是UnionFS

‘bootfs’ 用于系统引导的文件系统,包括 bootloader 和 kernel,容器启动完成后会被卸载以节省内存资源.bootloader主要是引导加载kernel,Linux刚启动会加载bootfs文件系统,在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs

‘rootfs’,在bootfs之上,表现为 Docker 容器的根文件系统。包含的就是典型Linux系统中的/dev,/proc,/bin,/etc等标准目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。传统模式中,系统启动时,内核挂载 rootfs 时会首先将其挂载为“只读”模式,完整性自检完成后将其挂载为读写模式(通过 UFS 技术挂载一个“可写” 层)

对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令,工具和程序就可以了,因为底层直接用Host和kernel,自己只需要提供rootfs就行了。由此可见对于不用的Linux发行版,bootfs基本是一致的,rootfs会有差别,因此不同的发行版可以共用bootfs。

  • 已有的分层只能读不能修改,上层镜像优先级大于底层镜像
  • 写实复制(先把文件下载,修改,而后在上传回去)

1.4 镜像分层

以我们的 pull 为例,在下载的过程中我们可以看到 docker 的镜像好像是在一层一层的在下载

镜像的分层:Docker 的镜像通过联合文件系统 ( union filesystem ) 将各层文件系统叠加在一起

1.5 为什么docker 镜像要采用这种分层结构

最大的一个好处就是:共享资源

比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只须在磁盘上保存一份 base镜像

同时内存中也只需要加载一份 base 镜像,就可以为所容器了。而且镜像的每一层都可以被共享。

1.6 镜像的特点

Docker 镜像都是只读的

当容器启动时,一个新的可写层被加载到镜像的顶部,这一层通常被称作 “容器层”,“容器层”之下的都叫 “镜像层”。

2. 构建镜像

2.1 docker build

命令格式为docker build [选项] <上下文路径/URL/->

Dockerfile是一个镜像构建命令集合的文本文件,下面是我们最常见的Dockerfile构建,假如我们目录下有一个文件Dockerfile

[root@localhost nginx_project]# ls  
Dockerfile
[root@localhost nginx_project]# docker build -t nginx:v1 .

docker build 命令最后有一个 .. 表示当前目录

通过build指定了目标镜像的标签为nginx:v1,以及Dockerfile的上下文context .

什么是docker上下文?

一个面向服务端的目录夹结构,除了Dockerfile,你的一切构建资源都应该在这个目录(指定的上下文)中。

上下文是递归处理的。因此, 如果是PATH则包含任何子目录,如果是一个URL则包含存储库及其子模块。

关键点,构建是由 Docker 守护程序运行,而不是由 CLI 运行,所以docker会把上下文资源打包传输给守护进程进行构建,为了减少不必要的臃肿,最好从一个空目录作为上下文开始,并将 Dockerfile 保存在该目录中。仅添加构建 Dockerfile 所需的文件。

我们可以使用-f选项指定dockerfile

[root@localhost folder]# docker build -f ../Dockerfile -t nginx:v1 .

使用多个-t选项保持多个tag

[root@localhost folder]# docker build -t nginx:v1 -t dockerhub.com/nginx:v2 .  
Sending build context to Docker daemon 1.583kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : run echo 123
---> Using cache
---> 3b636c79fbfa
Successfully built 3b636c79fbfa
Successfully tagged nginx:v1
Successfully tagged dockerhub.com/nginx:v2

这样就构建两个不同tag的同一ID镜像

[root@localhost folder]# docker images  
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerhub.com/nginx v2 3b636c79fbfa 23 minutes ago 133MB
nginx v1 3b636c79fbfa 23 minutes ago 133MB

2.2 BuildKit

buildkit将 Dockerfile 变成了 Docker 镜像。它不只是构建 Docker 镜像;它可以构建 OCI 图像和其他几种输出格式。

从版本18.09开始,Docker支持由moby / buildkit[3]项目提供的用于执行构建的新后端。与旧的实现相比,BuildKit后端提供了许多好处。例如,BuildKit可以:

  • 检测并跳过执行未使用的构建阶段。
  • 平行构建独立的构建阶段。
  • 在不同的构建过程中,只增加传输构建上下文中的更改文件。
  • 在构建上下文中检测并跳过传输未使用的文件。
  • 使用外部Dockerfile实现许多新功能。
  • 避免与API的其他部分(中间镜像和容器)产生副作用。
  • 优先处理您的构建缓存,以便自动修剪。

要使用BuildKit后端,只需要在调用 DOCKER_BUILDKIT=1 docker build 之前在CLI上设置环境变量DOCKER_BUILDKIT = 1。或者配置/etc/docker/daemon.json启用。

[root@localhost folder]# DOCKER_BUILDKIT=1 docker build -f ../Dockerfile -t nginx:v1 -t dockerhub.com/nginx:v2 .  
[+] Building 5.2s (6/6) FINISHED
=> [internal] load build definition from Dockerfile 0.7s
=> => transferring dockerfile: 118B 0.0s
=> [internal] load .dockerignore 0.6s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/nginx:latest 0.0s
=> [1/2] FROM docker.io/library/nginx 2.2s
=> [2/2] RUN echo 123 1.3s
=> exporting to image 0.5s
=> => exporting layers 0.2s
=> => writing image sha256:813b09c58322dce98ee28e717baeb9f3593ce3e46a032488949250f761004495 0.0s
=> => naming to docker.io/library/nginx:v1 0.0s
=> => naming to dockerhub.com/nginx:v2

3. DockerFile详解

3.1 DockerFile是什么

DockerFile 是用来构建 Docker 镜像的构建文件,是由一系列命令和参数构建的脚本

3.2 DockerFile 格式

1. 注释

一个标准的dockerfile,注释是必须的。

#这是dockerfile注释,dockerfile中指令以"CMD args"格式出现  
CMD args
CMD args
...

一个Dockerfile 第一个指令必须是FROM指令,用于指定基础镜像,那么基础镜像的父镜像从哪里来?答案是scratch带有该FROM scratch指令的 Dockerfile会创建一个基本映像

2. 解析器指令

解析器指令是可选的,会影响 aDockerfile中后续行的处理方式。解析器指令不会向构建添加层,也不会显示为构建步骤,单个指令只能使用一次。

dockerfile目前支持以下两个解析器指令:

  • syntax
  • escape

syntax

此功能仅在使用BuildKit[4]后端时可用,在使用经典构建器后端时会被忽略。

我们可以在dockerfile文件开头指定此dockerfile语法解析器,如下:

# syntax=docker/dockerfile:1  
# syntax=docker.io/docker/dockerfile:1
# syntax=example.com/user/repo:tag@sha256:abcdef...

通过syntax自定义 Dockerfile 语法解析器可以实现如下:

  • 在不更新 Docker 守护进程的情况下自动修复错
  • 确保所有用户都使用相同的解析器来构建您的 Dockerfile•无需更新 Docker 守护程序即可使用最新功能•在将新功能或第三方功能集成到 Docker 守护进程之前试用它们•使用替代的构建定义,或创建自己的定义[5]

官方dockerfile解析器:

  • docker/dockerfile:1 不断更新最新的1.x.x次要补丁版本
  • docker/dockerfile:1.2 保持更新最新的1.2.x补丁版本,一旦版本1.3.0发布就停止接收更新。•docker/dockerfile:1.2.1 不可变:从不更新1.2版本

比如我们使用1.2最新补丁版本,我们的Dockerfile如下:

#syntax=docker/dockerfile:1.2  
FROM busybox
run echo 123

我们启用buildkit构建

# DOCKER_BUILDKIT=1 docker build -t busybox:v1 .  
[+] Building 5.8s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.3s
=> => transferring dockerfile: 150B 0.0s
=> [internal] load .dockerignore 0.4s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.2 2.6s
=> CACHED docker-image://docker.io/docker/dockerfile:1.2@sha256:e2a8561e419ab1ba6b2fe6cbdf49fd92b95 0.0s
=> [internal] load metadata for docker.io/library/busybox:latest 0.0s
=> [1/2] FROM docker.io/library/busybox 0.3s
=> [2/2] RUN echo 123 1.1s
=> exporting to image 0.3s
=> => exporting layers 0.3s
=> => writing image sha256:bd66a3db9598d942b68450a7ac08117830b4d66b68180b6e9d63599d01bc8a04 0.0s
=> => naming to docker.io/library/busybox:v1

escape

通过escape定义dockerfile的换行拼接转义符

# escape=\

如果要构建一个window镜像就有大用处了,我们看下面dockerfile

FROM microsoft/nanoserver  
COPY testfile.txt c:\\
RUN dir c:\

由于默认转义符为\,则在构建的第二步step2会是这样COPY testfile.txt c:\RUN dir c:显然与我们的预期不符。

我们把转义符换成`号即可

# escape=`  

FROM microsoft/nanoserver
COPY testfile.txt c:\ `
RUN dir c:\

3. 类bash的环境变量

FROM busybox  
ENV FOO=/bar
WORKDIR ${FOO} # WORKDIR /bar
ADD . $FOO # ADD . /bar
COPY \$FOO /quux # COPY $FOO /quux

${variable_name}语法还支持bash 指定的一些标准修饰符:

  • ${variable:-word}表示如果variable变量被设置(存在),则结果将是该值。如果variable未设置,word则将是结果

  • ${variable:+word}表示如果variable被设置则为word结果,否则为空字符串。

4. .dockerignore

.dockerignore用于忽略CLI发送到docker守护进程的文件或目录。以下是一个.dockerignore文件

#.dockeringre可以有注释  
*.md
!README.md
temp?
*/temp*
*/*/temp*

4. DockerFile 命令

4.1 FROM

FROM 就是指定 基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。

初始化一个新的构建阶段,并设置基础镜像:

FROM [--platform=<platform>] <image> [AS <name>]  
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
  • 单个 Dockfile 可以多次出现 FROM,以使用之前的构建阶段作为另一个构建阶段的依赖项
  • AS name 表示为构建阶段命名,在后续 FROM 和 COPY --from=<name> 说明中可以使用这个名词,引用此阶段构建的映像
  • digest 其实就是就是根据镜像内容产生的一个 ID,只要镜像的内容不变 digest 也不会变
  • tag 或 digest 值是可选的。如果您省略其中任何一个,构建器默认使用一个 latest 标签。如果找不到该 tag 值,构建器将返回错误。
  • –platform 标志可用于在 FROM 引用多平台镜像的情况下指定平台。例如,linux/amd64、linux/arm64、 或 windows/amd64。

4.2 RUN

将在当前镜像之上的新层中执行命令,在 docker build时运行。

RUN /bin/bash -c 'source $HOME/.bashrc; \  
echo $HOME'

RUN 有两种形式:

  • RUN(shell 形式,命令在 shell 中运行,默认 /bin/sh -c 在 Linux 或 cmd /S /CWindows 上)
RUN yum install -y gcc
  • RUN ["executable", "param1", "param2"](执行形式)
RUN ["yum","install","-y","gcc"]

说明:

  • 可以使用 \(反斜杠)将单个 RUN 指令延续到下一行
  • RUN 在下一次构建期间,指令缓存不会自动失效。可以使用 –no-cache 标志使指令缓存无效
  • Dockerfile 的指令每执行一次都会在 Docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大,可以使用 && 符号连接命令,这样执行后,只会创建 1 层镜像

4.3 CMD

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

CMD指令有三种形式:

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

一个dockerfile中,应该只写一个CMD,如果有多个只有最后一个生效。在实际编写dockerfie时CMD命令常常用于为ENTRYPOINT提供默认值,后面我们会讲到。

与RUN相比,CMD在构建时不会执行任何操作,主要用于指定镜像的启动命令。CMD的启动命令可以被docker run 参数代替。

我们在dockerfile中添加如下CMD命令

CMD echo hello

构建镜像后,docker run 不添加参数,启动容器

[root@localhost dockerfiles]# docker run centos:v1  
hello

当我们在docker run 添加参数后

[root@localhost dockerfiles]# docker run centos_env:v1 echo container  
container

显然我们CMD命令echo hello已被docker run中的参数echo container取代。

4.4 LABEL

label用于添加镜像的元数据,采用key-value的形式。

LABEL <key>=<value>

比如我们添加如下LABEL

LABEL "miantainer"="iqsing.github.io"  
LABEL "version"="v1.2"
LABEL "author"="waterman&&iqsing"

为了防止创建三层,我们最好通过一个标签来写。

LABEL "miantainer"="iqsing.github.io" \  
"version"="v1.2" \
"author"="waterman&&iqsing"

我们通过docker inspect 来查看镜像label信息

#docker inspect centos_labels:v1  

"Labels": {
"author": "waterman&&iqsing",
"miantainer": "iqsing.github.io",
"org.label-schema.build-date": "20201204",
"org.label-schema.license": "GPLv2",
"org.label-schema.name": "CentOS Base Image",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vendor": "CentOS",
"version": "v1.2"
}

4.5 EXPOSE

EXPOSE <port> [<port>/<protocol>...]

Docker 容器在运行时侦听指定的网络端口。可以指定端口是监听TCP还是UDP,如果不指定协议,默认为TCP。

该 EXPOSE 指令实际上并未发布端口。要在运行容器时实际发布端口,docker run -P 来发布和映射一个或多个端口。

默认情况下,EXPOSE 假定 TCP。您还可以指定 UDP:

EXPOSE 80/udp

4.6 ENV

ENV <key>=<value> ... 首先方式  

ENV <key> <value>

通过ENV指定环境变量,将作用于在构建阶段的所有后续指令的环境中。

ENV username="iqsing"

这样当我们启动这个容器后可以查看到容器信息已经附带了ENV环境变量

"Env": [  
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"username=iqsing"
],

当然我们也可以在启动容器时添加环境变量

docker run --env <key>=<value>

另外如果只需要在镜像构建期间使用环境变量,更好的选择是使用ARG参数来处理

4.7 COPY

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

COPY package.json /usr/src/app/
COPY hom* /mydir/
COPY hom?.txt /mydir/

<源路径> 可以是多个,甚至可以是通配符

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

注: 使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。

4.8 ADD

ADD 指令和 COPY 的格式和性质基本一致。如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

4.9 ENTRYPOINT

exec首选和shell形式:

ENTRYPOINT ["executable", "param1", "param2"]  
ENTRYPOINT command param1 param2

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令

我们在dockerfile中添加ENTRYPOINT

ENTRYPOINT echo hello container

构建镜像并启动容器,可以看到docker run 中的参数并未取代ENTRYPOINT

[root@localhost dockerfiles]# docker run centos_entrtpoint:v1 echo hello docker  
hello container

这指令优秀的另一个地方在于可以和CMD指令做交互。让容器以应用或者服务运行。

经典操作:ENTRYPOINT + CMD = 默认容器命令参数

4.10 VOLUME

创建一个具有指定名称的挂载数据卷。

VOLUME ["/var/log/"]  
VOLUME /var/log

它的主要作用是:

  • 避免重要的数据,因容器重启而丢失
  • 避免容器不断变大

volume指令可以用于创建存储卷,我来看一下实例:

FROM centos  
RUN mkdir /volume
RUN echo "hello world" > /volume/greeting
VOLUME /volume

构建镜像后,创建一个容器

[root@localhost dockerfiles]# docker create --name centos_volume centos_volue:v1  
[root@localhost dockerfiles]# docker inspect centos_volume

"Mounts": [
{
"Type": "volume",
"Name": "494cdb193984680045c36a16bbc2b759cf568b55c7e9b0852ccf6dff8bf79c46",
"Source": "/var/lib/docker/volumes/494cdb193984680045c36a16bbc2b759cf568b55c7e9b0852ccf6dff8bf79c46/_data",
"Destination": "/volume",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],

这样我们就通过VOLUME指令创建一个存储卷,你可以通过--volumes-from共享这个容器。

4.11 USER

指定指令集所属用户和组。组默认为root。可以作用于RUNCMDENTRYPOINT它们后面的指令。

USER <user>[:<group>]  

USER <UID>[:<GID>]

4.12 WORKDIR

指定指令集所在的工作目录,若目录不存在将会自动创建。可作用于RUNCMD, ENTRYPOINTCOPYADD

WORKDIR /path/to/workdir

4.13 ARG

定义变量,与 ENV 作用相同,不过 ARG 变量不会像 ENV 变量那样持久化到构建好的镜像中。

ARG <name>[=<default value>]

Docker 有一组预定义的 ARG 变量,您可以在 Dockerfile 中没有相应指令的情况下使用这些变量。

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

要使用这些,请使用 –build-arg 标志在命令行上传递它们,例如:

docker build --build-arg HTTPS_PROXY=https://my-proxy.example.com .

4.14 STOPSIGNAL

设置将发送到容器退出的系统调用信号。该信号可以是与内核系统调用表中的位置匹配的有效无符号数,例如 9,或格式为 SIGNAME 的信号名称,例如 SIGKILL。

STOPSIGNAL signal

默认的 stop-signal 是 SIGTERM,在 docker stop 的时候会给容器内 PID 为 1 的进程发送这个 signal,通过 –stop-signal 可以设置自己需要的 signal,主要目的是为了让容器内的应用程序在接收到 signal 之后可以先处理一些事物,实现容器的平滑退出,如果不做任何处理,容器将在一段时间之后强制退出,会造成业务的强制中断,默认时间是 10s。

4.15 HEALTHCHECK

用于指定某个程序或者指令来监控 Docker 容器服务的运行状态。

HEALTHCHECK指令有两种形式:

  • HEALTHCHECK [OPTIONS] CMD command (通过在容器内运行命令来检查容器健康状况)
  • HEALTHCHECK NONE (禁用从基础镜像继承的任何健康检查)

OPTIONS支持如下参数:

  • --interval=DURATION(默认值:30s
  • --timeout=DURATION(默认值:30s
  • --start-period=DURATION(默认值:0s
  • --retries=N(默认值:3

比如我们可以添加如下参数用于检查web服务:

HEALTHCHECK --interval=5m --timeout=3s \  
CMD curl -f http://localhost/ || exit 1

每五分钟左右检查一次web服务器能否在3s内响应。如果失败则返回状态码1

命令的退出状态指示容器的健康状态。可能的值为:

  • 0:成功 - 容器运行良好,可以使用
  • 1:不健康 - 容器无法正常工作
  • 2:reserved - 不要使用这个退出代码

4.16 ONBUILD

将一个触发指令添加到镜像中,以便稍后在该镜像用作另一个构建的基础时执行。也就是另外一个 dockerfile FROM 了这个镜像的时候执行。

ONBUILD ADD . /app/src  
ONBUILD RUN /usr/local/bin/python-build --dir /app/src

参考感谢
Dockerfile文件全面详解 (qq.com)
docker容器dockerfile详解 (qq.com)
dockerfile · 语雀 (yuque.com)