# 镜像

# 标识符

每个 Docker 镜像有三种标识符:

  • <image_id> :镜像 id ,取自镜像配置文件的 SHA256 哈希值的开头 n 位。

    • 这里未指定完整的哈希值,只能在本机标识镜像,不能在 docker pull、docker push 时标识镜像。
  • <image_name>@<digest> :镜像名加哈希值。

    • 这里的 digest 是指镜像 manifest 的哈希值,格式为 sha256:<hash> 。例如:
      nginx@sha256:11bf3beaa91524d60236685b2c05063c248d22b60247d63b9c35712cf761f7ed
      
  • <image_name>[:tag] :镜像名加标签,由用户自定义。

    • 使用 <image_name>[:tag] 作为镜像的标识符,比 <image_id><image_name>@<digest> 更方便用户记忆,但 tag 可能被修改、删除,不一定可靠。
    • 镜像名(image_name)是一个字符串,可包含字符 [0-9A-Za-z._-] 。通常用斜杆 / 划分多个层级。例如:
      nginx:1.23
      docker.io/library/nginx:1.23
      
    • 标签(tag)用于区分同一 image_name 的镜像的不同版本。例如:
      nginx:1.23
      nginx:1.24
      
  • image_tag 的取值通常来自 git_tag 或 build_date 。

    • 省略 image_tag 时,默认指定 image_tag 为 latest 。
      • 例如执行 docker pull nginx 相当于 docker pull nginx:latest
      • 有的开源软件制作镜像时,会将 latest 标签指向最新一个版本,便于用户拉取。
      • 严格来说,用户不应该拉取 latest 版本的镜像,否则在不同时间拉取的 latest 版本可能不同,即镜像的哈希值不同。
    • 悬空镜像(dangling images):一些只有哈希值的镜像。如果查看其 image_name 和 tag ,则会显示 <none>:<none>

# manifest

  • 在 hub.docker.com 查看一个镜像时,会显示一份镜像索引(image index),说明该镜像支持运行在哪些平台(platform)。
    • 这里 platform 是指主机的操作系统和 CPU 架构,比如 linux/amd64 、linux/arm64 。
    • 一般的开源软件会为不同 platform 的用户分别编译二进制程序,制作成不同哈希值的镜像。因此 image index 中包含多个镜像。
  • image index 中每个镜像都有一份 JSON 格式的镜像清单(image manifest),记录该镜像的简要信息。例如:
    {
        "schemaVersion": 2,     // manifest 的语法版本
        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",  // 整个 manifest 的数据类型
        "config": {
            "mediaType": "application/vnd.docker.container.image.v1+json",    // config 的数据类型
            "size": 6844,
            "digest": "sha256:faff56ad2ef574b8260716706d56e0277520d17103743c3e94355391005e4cdb"
        },
        "layers": [
            {
                "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
                "size": 76097157,
                "digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc"
            },
            {
                "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
                "size": 122,
                "digest": "sha256:77c5a601c050c59fe3abd066973abc67f6278e7ee3bf21f004e991bd82900fb4"
            }
        ]
    }
    
    • 执行 docker pull 命令时,会先获取镜像的 manifest ,然后根据 manifest 拉取镜像的各个 layer ,并检查镜像的 config、layers 的 size、digest 是否正确。
    • 上例中的 mediaType 采用 docker 标准,说明该镜像由 docker 命令制作,兼容 OCI 标准,但不是 OCI 标准。
      • 严格来说,OCI 镜像不需要声明整个 manifest 的 mediaType ,且 config、layers 的 mediaType 采用 OCI 标准,例如 "mediaType": "application/vnd.oci.image.config.v1+json"
    • OCI 标准的 manifest 原本专用于描述容器镜像。但利用 mediaType 字段,可用 manifest 描述其它二进制工件。
      • 例如 helm 用 "mediaType": "application/vnd.cncf.helm.config.v1+json" 描述 Chart 。使得 Chart 可以存储到符合 OCI 标准的镜像仓库。

# 查看

docker
      image
            ls                # 显示本机的镜像(默认不显示悬空镜像)。可简写为 docker images 命令
                -a            # 显示所有的镜像
            history <image>   # 显示镜像的构建步骤(按时间倒序排列),包含每个步骤增加的 layer 大小
                --no-trunc

            rm <image>...     # 删除本机的镜像(只能删除未被容器使用的),可简写为 docker rmi 命令
            prune             # 删除所有悬空镜像
                -a            # 删除所有未被容器使用的镜像

      tag <image> <image_name>[:tag]    # 给镜像添加另一个镜像名和标签。如果已有镜像占用目标标签,则将其标签改为 <none>

      inspect <image>                   # 查看本机某个镜像的详细信息
      manifest inspect <image>          # 查看远程仓库中某个镜像的清单,但只能识别 docker 标准的 mediaType
      buildx imagetools inspect <image> # 查看远程仓库中某个镜像的清单

# 拉取

docker
      login  <domain>           # 登录一个镜像仓库。登录凭证会保存在 $HOME/.docker/config.json 中
          -u <username>
          -p <password>
      pull    <image>           # 从镜像仓库拉取镜像,即下载镜像
          --platform <os/arch>  # 拉取指定平台的镜像。默认只拉取兼容本机的镜像
      push    <image>           # 推送镜像到镜像仓库
      search  <name>            # 按名称搜索 hub.docker.com 中的镜像
  • 例:

    docker pull nginx:1.23              # 拉取镜像
    docker pull nginx                   # 相当于 docker pull nginx:latest
    docker tag  nginx:1.23 nginx:test   # 添加标签
    
  • 例:拉取镜像的过程

    [root@CentOS ~]# docker pull nginx:1.23
    1.20: Pulling from library/nginx            # 开始从镜像仓库拉取镜像
    e5ae68f74026: Pull complete                 # 拉取镜像中的一个 layer ,如果本机已存在则不会拉取
    2dc3587e7d0c: Pull complete
    b8258363a4a3: Pull complete
    963807cfb489: Pull complete
    5faf54adf667: Pull complete
    07bd53fd2d21: Pull complete
    Digest: sha256:71a1217d769cbfb5640732263f81d74e853f101b7f2b20fcce991a22e68adbc7   # 镜像的 manifest 的哈希值
    Status: Downloaded newer image for nginx:1.23
    docker.io/library/nginx:1.23
    
    • 拉取每个 layer 时,分为多个步骤:
      Downloading           # 下载 layer 的压缩包
      Download complete     # 下载完毕
      Extracting            # 解压 layer 并导入,存储在宿主机上
      Pull complete         # 拉取完毕
      
  • 可以将宿主机上存储的 docker 镜像,推送到镜像仓库服务器进行存储。

    • 默认采用官方镜像仓库 docker.io ,允许未登录用户 pull 公开镜像。
      • 官方还提供了 Web 页面 hub.docker.com ,用于搜索镜像。
      • 用户也可以自己部署仓库服务器,比如 harbor 。
    • 为了区分不同的镜像仓库,需要在镜像名的前面加上仓库地址:
      docker pull harbor.test.com/project1/nginx   # 使用指定的镜像仓库,格式为 <仓库域名>/<命名空间>/<镜像名>
      
    • 默认采用官方镜像仓库 docker.io 的 library 命名空间中的镜像,因此以下三种写法的指向相同:
      docker pull nginx
      docker pull docker.io/nginx
      docker pull docker.io/library/nginx
      

# 导出

  • 一个 Docker 镜像在宿主机上存储为一些零散文件,包含一组 layer 和配置文件,可用以下命令导出:
    docker save -o image.tar <image>...         # 将镜像打包成 tar 格式
    docker save <image>... | gzip > image.tgz   # 打包并压缩,大概压缩到 40% 大小
    
    docker load -i image.tar                    # 导入镜像
    
  • 例:
    [root@CentOS ~]# docker save -o nginx.tar nginx:latest
    [root@CentOS ~]# ls -lh
    total 131M
    -rw-------. 1 root root 131M Mar 28 16:04 nginx.tar
    [root@CentOS ~]# tar -tf nginx.tar
    28d499c51144128e64b6ffefa6c714bbfaf3e55772b080d1b0636f1971cb3203/           # 每个目录对应一层 layer 。目录名是此时导出文件的哈希值,并不等于 layer.tar 的哈希值
    28d499c51144128e64b6ffefa6c714bbfaf3e55772b080d1b0636f1971cb3203/VERSION    # 该 layer 的格式规范,目前为 1.0
    28d499c51144128e64b6ffefa6c714bbfaf3e55772b080d1b0636f1971cb3203/json       # 该 layer 的配置文件,记录了其 id、父级 layer 的 id、构建时的 container_config
    28d499c51144128e64b6ffefa6c714bbfaf3e55772b080d1b0636f1971cb3203/layer.tar  # 该 layer 包含的所有文件
    40aef34ac16b8c7eee6da1869452f5c9b9963ab583415d4999565738c719ded9/
    40aef34ac16b8c7eee6da1869452f5c9b9963ab583415d4999565738c719ded9/VERSION
    40aef34ac16b8c7eee6da1869452f5c9b9963ab583415d4999565738c719ded9/json
    40aef34ac16b8c7eee6da1869452f5c9b9963ab583415d4999565738c719ded9/layer.tar
    456351a127e9a9ce4cc79f7f6ad9f401d1714e514780f1603fa0b263119e329b/
    456351a127e9a9ce4cc79f7f6ad9f401d1714e514780f1603fa0b263119e329b/VERSION
    456351a127e9a9ce4cc79f7f6ad9f401d1714e514780f1603fa0b263119e329b/json
    456351a127e9a9ce4cc79f7f6ad9f401d1714e514780f1603fa0b263119e329b/layer.tar
    9000127bc2e7878a10491bb7a16a4b5874e4bdf6a01952d14211fad55defdd0a/
    9000127bc2e7878a10491bb7a16a4b5874e4bdf6a01952d14211fad55defdd0a/VERSION
    9000127bc2e7878a10491bb7a16a4b5874e4bdf6a01952d14211fad55defdd0a/json
    9000127bc2e7878a10491bb7a16a4b5874e4bdf6a01952d14211fad55defdd0a/layer.tar
    b526b761d738d1fba0774ea5af56ae1e664c812c6ce75743d74773cb3867bf7b/
    b526b761d738d1fba0774ea5af56ae1e664c812c6ce75743d74773cb3867bf7b/VERSION
    b526b761d738d1fba0774ea5af56ae1e664c812c6ce75743d74773cb3867bf7b/json
    b526b761d738d1fba0774ea5af56ae1e664c812c6ce75743d74773cb3867bf7b/layer.tar
    b8cf2cbeabb915843204ceb7ef0055fecadd55c2b0c58ac030e01fe75235885a.json       # 该镜像的配置文件。该文件的 SHA256 哈希值被用作文件名、image id
    c0b073121bb2a6106dae6af85ade7274253f26626661e6e3cb20b0fa7fb59475/
    c0b073121bb2a6106dae6af85ade7274253f26626661e6e3cb20b0fa7fb59475/VERSION
    c0b073121bb2a6106dae6af85ade7274253f26626661e6e3cb20b0fa7fb59475/json
    c0b073121bb2a6106dae6af85ade7274253f26626661e6e3cb20b0fa7fb59475/layer.tar
    manifest.json                                                               # 该镜像的清单
    repositories
    
    • 镜像的配置文件 <sha256>.json 的内容示例:
      {
        "architecture": "amd64",
        "config": {                   // 记录该镜像的配置,主要由 Dockerfile 决定
          "Hostname": "",
          "Domainname": "",
          "User": "",
          "AttachStdin": false,
          "AttachStdout": false,
          "AttachStderr": false,
          "ExposedPorts": {
            "80/tcp": {}
          },
          "Tty": false,
          "OpenStdin": false,
          "StdinOnce": false,
          "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "NGINX_VERSION=1.20.2", "NJS_VERSION=0.7.0", "PKG_RELEASE=1~bullseye"],
          "Cmd": ["nginx", "-g", "daemon off;"],
          "Image": "sha256:8e9a1b312fca0584850ce522438997f952010118d95f408add7eed34b8a2462d",
          "Volumes": null,
          "WorkingDir": "",
          "Entrypoint": ["/docker-entrypoint.sh"],
          "OnBuild": null,
          "Labels": {
            "maintainer": "NGINX Docker Maintainers \[email protected]\u003e"
          },
          "StopSignal": "SIGQUIT"
        },
        "container": "d9a5e6a8c2e78750b6e1cf3e1c62542d0d9bac5e5d714744a652974b20b3f987",    // 记录构建镜像时的最后一个中间容器
        "container_config": {
          "Hostname": "d9a5e6a8c2e7",
          ...
        },
        "history": [{       // 记录该镜像的构建步骤,按时间顺序排列
          "created": "2021-11-17T02:20:41.91188934Z",
          "created_by": "/bin/sh -c #(nop) ADD file:a2405ebb9892d98be2eb585f6121864d12b3fd983ebf15e5f0b7486e106a79c6 in / "
        }, ...
        {
          "created": "2021-11-17T10:39:44.423437008Z",
          "created_by": "/bin/sh -c #(nop)  CMD [\"nginx\" \"-g\" \"daemon off;\"]",
          "empty_layer": true
        }],
        "os": "linux",
        "rootfs": {         // 记录组成该镜像的各个 layer 的哈希值。创建容器时需要按先后顺序载入这些 layer ,生成 RootFS 文件系统
          "type": "layers",
          "diff_ids": ["sha256:e1bbcf243d0e7387fbfe5116a485426f90d3ddeb0b1738dca4e3502b6743b325", "sha256:72e7342f59d8d99e69f1a39796e9023fee99f2b9c72bfe75cd7cc8c86b43c918", ...]
        }
      }
      

# 制作

Docker 镜像主要有两种制作方式:

  • 将一个容器提交为镜像:

    docker commit <container> <image_name>[:tag]
    
    • 每次 commit 时,会在原镜像外部加上一层新的 layer 。因此 commit 次数越多,镜像的体积越大。
  • 编写 Dockerfile 文件,然后根据它构建镜像。