文章目录
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.04
和ubuntu: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 上有很多高质量的官方镜像,包括服务类镜像,如nginx
、redis
、httpd
等;也有一些方便开发、构建、运行各种语言应用的镜像,如node
、python
等;还包括一些更为基础的操作系统镜像,例如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