Docker从入门到实践②:使用镜像

本文详细介绍了Docker的镜像获取、列出、删除操作,重点讲解了Dockerfile的使用,包括FROM指定基础镜像、RUN执行命令、构建镜像以及创建支持多种系统架构的manifest列表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 获取镜像

$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

# e.g.
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04

具体的选项可以通过docker pull --help命令看到。

镜像名称的格式:

  • Docker镜像仓库地址:地址的格式一般是<域名/IP>[:端口号],默认地址是 Docker Hub(docker,io)。
  • 仓库名:仓库名是两段式名称,即<用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

2 列出镜像

$ docker image ls
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
redis                latest              5f515359c7f8        5 days ago          183 MB
nginx                latest              05a60462f8ba        5 days ago          181 MB
mongo                3.2                 fe9198c04d62        5 days ago          342 MB
<none>               <none>              00285df0df87        5 days ago          342 MB
ubuntu               18.04               329ed837d508        3 days ago          63.3MB
ubuntu               bionic              329ed837d508        3 days ago          63.3MB

列表包含仓库名、标签、镜像ID、创建时间以及大小。

镜像ID是镜像唯一的标识,一个镜像可以对应多个标签。
可以看到ubuntu:18.04ubuntu:bionic拥有相同的ID,因为它们对应的是同一个镜像。

2.1 虚悬镜像

<none>               <none>              00285df0df87        5 days ago          342 MB

由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为<none>的镜像。这类无标签镜像也被称为虚悬镜像(dangling image)。

除了docker pull可能会导致这种情况,docker build也同样可以导致这种现象。

可以用如下命令专门显示虚悬镜像:

$ docker image ls -f dangling=true
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              00285df0df87        5 days ago          342 MB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。

$ docker image prune

2.2 中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用中间层镜像。在使用一段时间后,可能会看到一些依赖的中间层镜像,默认列出镜像只显示顶层镜像,如果需要显示中间层,需要加上参数-a

$ docker image ls -a

与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,被其他镜像所依赖,删除会导致上层镜像因为依赖丢失而出错

2.3 镜像体积

docker image ls列表中的镜像体积总和并非是所有镜像实际硬盘消耗,由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会拥有共同的层。Docker 使用 Union FS,相同的层只需要保存一份即可,实际镜像硬盘占用空间可能远小于列表中镜像大小的总和。

# 用于查看镜像、容器、数据卷所占用的空间
$ docker system df

TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              24                  0                   1.992GB             1.992GB (100%)
Containers          1                   0                   62.82MB             62.82MB (100%)
Local Volumes       9                   0                   652.2MB             652.2MB (100%)
Build Cache                                                 0B                  0B

3 删除本地镜像

$ docker image rm [选项] <镜像1> [<镜像2> ...]

镜像的删除可以通过镜像ID、镜像名或者镜像摘要删除。

# 查看镜像摘要
$ docker image ls --digests
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB

$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

3.1 Untagged 和 Deleted

仔细观察输出信息,可以看到删除行为分为两类,一类是Untagged,另一类是Deleted

并非所有的docker image rm都会产生删除镜像的行为,有可能仅仅是取消了某个标签。

镜像的唯一标识是其ID和摘要,一个镜像可以有多个标签。

当我们使用命令删除镜像时,实际上是在要求删除某个标签的镜像。所以首先要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的Untagged的信息。
因为一个镜像可以对应多个标签,因此删除指定标签后,Deleted行为不一定发生。

由于镜像为多层存储结构,删除时也是从上层向基础层方向依次进行判断删除,很有可能某个其它镜像正依赖于当前镜像的某一层,这种情况下也不会触发删除行为。

这也是为什么有时候明明没有标签指向该镜像,但它依然存在的原因,也是为什么有时候所删除的层数和自己docker pull看到的层数不一样的原因。

除了镜像依赖以外,还要注意容器对镜像的依赖。如果有使用该镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。因此如果这些容器是不需要的,应该先删除容器,后删除镜像。

容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。

3.2 批量删除

使用docker image ls -q配合docker image rm使用,就可以批量的删除希望删除的镜像。

比如删除所有仓库名为redis的镜像:

$ docker image rm $(docker image ls -q redis)

或者删除所有在mongo:3.2之前的镜像:

$ docker image rm $(docker image ls -q -f before=mongo:3.2)

4 使用 Dockerfile 定制镜像

镜像的定制实际上就是定制每一层所添加的配置、文件,如果使用docker commit进行定制,会存在无法重复、镜像构建不透明以及镜像体积大等问题;而使用 Dockerfile 将有效解决这个问题。

4.1 FROM指定基础镜像

一个 Dockerfile 中FROM是必备的指令,并且必须是第一条指令。

在 Docker Hub 上有很多高质量的官方镜像,包括服务类镜像,如nginxredishttpd等;也有一些方便开发、构建、运行各种语言应用的镜像,如nodepython等;还包括一些更为基础的操作系统镜像,例如ubuntu

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

如果使用scratch为基础镜像的话,意味着将不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,因此直接FROM scratch会让镜像体积更加小巧。

使用 Go 语言开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

4.2 RUN 执行命令

RUN指令是定制镜像时最常用的指令之一,其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。例如:
    RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
    
  • exec 格式:RUN ["可执行文件”, "参数1", "参数2"],更类似于函数调用的格式。

Dockerfile中每一个指令都会建立一层,每一个RUN的行为,都会新建立一层,在其上执行命令,执行结束后,commit这一层的修改,构成新的镜像。

Union FS 是有最大层数限制的,比如AUFS,曾经是最大不得超过42层,现在是不得超过127层。

为避免冗余,建议写法如下:

FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

这里没有使用很多个RUN,而是仅仅使用一个RUN指令,并使用&&将各个所需命令串联起来,将7层简化为1层。在撰写 Dockerfiile 的时候,要经常提醒自己这并不是在写 shell 脚本,而是在定义每一层该如何构建。

可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件。

镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。避免镜像臃肿。

4.3 构建镜像

在 Dockerfile 文件所在目录执行:

docker build [选项] <上下文路径/URL/->

# e.g.
$ docker build -t nginx:v3 .	# 指定了最终镜像的名称
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

在 Step 2 中,如同我们之前所说的那样,RUN指令启动了一个容器9cdc27646c7b,执行了所要求的命令,并最后提交了这一层44aa4490ce2c,随后删除了所用到的这个容器9cdc27646c7b

4.4 镜像构建上下文(Context)

在理解什么是上下文之前,先要理解docker build的工作原理。

Docker 在运行时分为 Docker 引擎(即服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如docker命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。

因此,虽然表面上我们好像是在本机执行各种docker功能,但实际上,一切都是使用的远程调用形式在服务端(Docker引擎)完成。

当进行镜像构建的时候,并非所有定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过COPY指令、ADD指令等。而docker build命令构建镜像,实际上是在服务端也就是 Docker 引擎中构建的,如何让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build命令得知这个路径后,会将路径下的所有内容打包上传给 Docker 引擎。例如:

COPY ./package.json /app/

这里的package.json,并非是在docker build命令所在目录下,也不是 Dockerfile 所在目录下,而是上下文目录下的。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西不希望构建时传给 Docker 引擎,那么可以用.gitignore一样的语法写一个.dockerignore,用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f …/Dockerfile.php 参数指定某个文件作为 Dockerfile。

当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。


5 构建多种系统架构支持的 Docker 镜像

使用镜像创建一个容器,该镜像必须与 Docker 宿主机系统架构一致,例如 Linux x86_64架构的系统中只能使用Linux x86_64的镜像创建容器。

Windows、MacOS除外,其使用了binfmt_msc提供了多种架构支持,在Windows、MacOS系统上可以运行arm等其他架构的镜像。

当用户获取一个镜像时,Docker 引擎会首先查找该镜像是否有manifest列表,如果有的话 Docker 引擎会按照 Docker运行环境(系统及架构)查找出对应镜像,如果没有的话会直接获取镜像。以golang:alpine的部分manifest列表为例:

{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1365,
         "digest": "sha256:5e28ac423243b187f464d635bcfe1e909f4a31c6c8bce51d0db0a1062bec9e16",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1365,
         "digest": "sha256:2945c46e26c9787da884b4065d1de64cf93a3b81ead1b949843dda1fcd458bae",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v7"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1365,
         "digest": "sha256:69f5907fa93ea591175b2c688673775378ed861eeb687776669a48692bb9754d",
         "platform": {
            "architecture": "s390x",
            "os": "linux"
         }
      }
   ]
}

可以看出manifest列表中包含了不同系统架构所对应的镜像digest值。

下面介绍如何使用$docker manifest命令创建并推送manifest列表到 Docker Hub。

5.1 构建镜像

首先在 Linux x86_64 构建username/x8664-test镜像。并在 Linux arm64v8 中构建username/arm64v8-test镜像,构建好之后推送到 Docker Hub。

5.2 创建 manifest 列表

# 格式
$ docker manifest create MANIFEST_LIST MANIFEST [MANIFEST...]
$ docker manifest create username/test \
      username/x8664-test \
      username/arm64v8-test

5.3 设置 manifest 列表

$ docker manifest annotate [OPTIONS] MANIFEST_LIST MANIFEST
$ docker manifest annotate username/test \
      username/x8664-test \
      --os linux --arch x86_64

$ docker manifest annotate username/test \
      username/arm64v8-test \
      --os linux --arch arm64 --variant v8

5.4 推送 manifest

# 查看 manifest 列表
$ docker manifest inspect username/test

# 推送 manifest 列表
$ docker manifest push username/test
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值