1.4 镜像

1.4 镜像 #

1.4.1 什么是镜像 #

镜像和常见的 tar、rpm、deb 等安装包一样,都打包了应用程序,但最大的不同点在于它里面不仅有基本的可执行文件,还有应用运行时的整个系统环境。这就让镜像具有了非常好的跨平台便携性和兼容性,能够让开发者在一个系统上开发(例如 Ubuntu),然后打包成镜像,再去另一个系统上运行(例如 CentOS),完全不需要考虑环境依赖的问题,是一种更高级的应用打包方式。

1.4.2 镜像的内部机制 #

容器镜像内部并不是一个平坦的结构,而是由许多的镜像层组成的,每层都是只读不可修改的一组文件,相同的层可以在镜像之间共享,然后多个层像搭积木一样堆叠起来,再使用一种叫 “Union FS 联合文件系统” 的技术把它们合并在一起,就形成了容器最终看到的文件系统。

Docker 会检查是否有重复的层,如果本地已经存在就不会重复下载,如果层被其他镜像共享就不会删除,这样就可以节约磁盘和网络成本。

1.4.3 容器化应用 #

“容器化的应用” 或 “应用的容器化”,就是指应用程序不再直接和操作系统打交道,而是封装成镜像,再交给容器环境去运行。镜像就是静态的应用容器,容器就是动态的应用镜像,两者互相依存,互相转化,密不可分。

1.4.4 镜像的命名规则 #

镜像的完整名字由两个部分组成,名字和标签,中间用:连接起来。

名字表明了应用的身份,比如 busybox、Alpine、Nginx、Redis 等等。

标签(tag)可以理解成是为了区分不同版本的应用而做的额外标记,任何字符串都可以,比如 3.15 是纯数字的版本号、jammy 是项目代号、1.21-alpine 是版本号加操作系统名等等。其中有一个比较特殊的标签叫 “latest”,它是默认的标签,如果只提供名字没有附带标签,那么就会使用这个默认的 “latest” 标签。

通常来说,镜像标签的格式是应用的版本号加上操作系统。版本号基本上都是主版本号 + 次版本号 + 补丁号的形式,有的还会在正式发布前出 rc 版(候选版本,release candidate)。而操作系统的情况略微复杂,因为各个 Linux 发行版的命名方式 “花样” 太多。Alpine、CentOS 的命名比较简单明了,就是数字的版本号,像 alpine3.15 ,而 Ubuntu、Debian 则采用了代号的形式。比如 Ubuntu 18.04 是 bionic,Ubuntu 20.04 是 focal,Debian 9 是 stretch,Debian 10 是 buster,Debian 11 是 bullseye。另外,有的标签还会加上 slim、fat,来进一步表示这个镜像的内容是经过精简的,还是包含了较多的辅助工具。通常 slim 镜像会比较小,运行效率高,而 fat 镜像会比较大,适合用来开发调试。

如上图,REPOSITORY 列就是镜像的名字,TAG 就是这个镜像的标签。IMAGE ID 是镜像唯一的标识,就好像是身份证号一样。这里用 REPOSITORY 而不是 IMAGE 是因为 Docker 认为一系列同名但不同版本的镜像构成了一个集合,就好像是一个 “镜像存储库”,用 REPOSITORY 来表述更加恰当,相当于 GitHub 上的 Repository。

同一个镜像也可以打上不同的标签,也就是说 IMAGE ID 一样,但是 TAG 可以不一样。如果一个镜像同时具有多个标签就不能直接使用 IMAGE ID 来删除,Docker 会提示镜像存在多个引用(即标签),拒绝删除。

IMAGE ID 还有一个好处,因为它是十六进制形式且唯一,Docker 特意为它提供了 “短路” 操作,在本地使用镜像的时候,我们不用像名字那样要完全写出来这一长串数字,通常只需要写出前三位就能够快速定位,在镜像数量比较少的时候用两位甚至一位数字也许就可以了。

1.4.5 Dockerfile #

Dockerfile 是一个纯文本,里面记录了一系列的构建指令,比如选择基础镜像、拷贝文件、运行脚本等等,每个指令都会生成一个 Layer,而 Docker 顺序执行这个文件里的所有步骤,最后就会创建出一个新的镜像出来。比如:

# Dockerfile.busybox
FROM busybox                  # 选择基础镜像
CMD echo "hello world"        # 启动容器时默认运行的命令

第一条指令是 FROM,所有的 Dockerfile 都要从它开始,表示选择构建使用的基础镜像,

第二条指令是 CMD,它指定 docker run 启动容器时默认运行的命令,这里使用了 echo 命令,输出 “hello world” 字符串。

利用 docker build 命令可以根据 Dockerfile 文件创建出镜像。-f 参数可以指定 Dockerfile 文件名,后面必须跟一个文件路径,叫做 “构建上下文”(build’s context),这里只是一个简单的点号,表示当前路径

新的镜像暂时还没有名字,用 docker images 会看到是

创建镜像的时候应当尽量使用 -t 参数,为镜像起一个有意义的名字,方便管理。名字必须要符合命名规范,用 : 分隔名字和标签,如果不提供标签默认就是 “latest”。

只有名字 my1234 没有 tag,会默认加上 latest tag:

编写规范 #

Dockerfile 中的指令,RUN, COPY, ADD 会生成新的镜像层(其它指令只会产生临时层,不影响构建大小),所以在 Dockerfile 里最好不要滥用指令,尽量精简合并,否则太多的层会导致镜像非常臃肿。

构建镜像的第一条指令必须是 FROM,所以基础镜像的选择非常关键。如果关注的是镜像的安全和大小,那么一般会选择 Alpine;如果关注的是应用的运行稳定性,那么可能会选择 Ubuntu、Debian、CentOS。

如果需要将源码、配置文件等打包进镜像里,可以使用 COPY 命令,它的用法和 Linux 的 cp 差不多,不过拷贝的源文件必须是 “构建上下文” 路径里的,不能随意指定文件。

COPY ./a.txt  /tmp/a.txt    # 把构建上下文里的 a.txt 拷贝到镜像的 /tmp 目录
COPY /etc/hosts  /tmp       # 错误!不能使用构建上下文之外的文件

RUN 命令可以执行任意的 Shell 命令,比如更新系统、安装应用、下载文件、创建目录、编译程序等等,实现任意的镜像构建步骤。

Dockerfile 里一条指令只能是一行,所以有的 RUN 指令会在每行的末尾使用续行符 \,命令之间也会用 && 来连接,这样保证在逻辑上是一行。

RUN apt-get update \
    && apt-get install -y \
    build-essential \
    && cd /tmp \

有的时候在 Dockerfile 里写超长的 RUN 指令很不美观,而且一旦写错了,每次调试都要重新构建也很麻烦,可以采用一种变通的技巧:把这些 Shell 命令集中到一个脚本文件里,用 COPY 命令拷贝进去再用 RUN 来执行:

COPY setup.sh  /tmp/                # 拷贝脚本到/tmp目录

RUN cd /tmp && chmod +x setup.sh \  # 添加执行权限
    && ./setup.sh && rm setup.sh    # 运行脚本然后再删除

参数化 #

RUN 指令实际上就是 Shell 编程,在 Shell 编程中有变量的概念,可以实现参数化运行,这在 Dockerfile 里也可以做到,需要使用两个指令 ARG 和 ENV。它们区别在于:

ARG 创建的变量只在镜像构建过程中可见,容器运行时不可见,而 ENV 创建的变量不仅能够在构建镜像的过程中使用,在容器运行时也能够以环境变量的形式被应用程序使用

下面是一个简单的例子,使用 ARG 定义了基础镜像的名字(可以用在 “FROM” 指令里),使用 ENV 定义了两个环境变量:

ARG IMAGE_BASE="node"
ARG IMAGE_TAG="alpine"

ENV PATH=$PATH:/tmp
ENV DEBUG=OFF

参考 #