镜像是 Docker 容器的基石,容器是镜像的实例,有了镜像才能启动容器。
镜像到底包含什么呢?容器为什么是轻量级的虚拟化呢?

首先从一个最小的镜像 hello-world 讲起,hello-world 镜像仅有 1.85kB,根据经验它肯定是不包括 Linux 的内核的,因为现在 Linux 内核大小至少 100MB 以上。
这么小的镜像,它能运行,是一个完整的镜像,他是怎么构建出来的呢?
我们可以上到 github 中查看这个镜像的源码
打开里面的 Dockerfile,会这个镜像的构建文件仅有三行,第一行从空白镜像开始构建,第二行拷贝二进制 hello 程序,第三行运行 hello 程序。
FROM scratch
COPY hello /
CMD ["/hello"]/hello 就是文件系统的全部内容,连最基本的 /bin,/usr, /lib, /dev 都没有。
hello-world 虽然是一个完整的镜像,但它并没有什么实际用途。
通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件。这样的镜像我们称作 base 镜像。
什么是 base 镜像?Base 镜像指不依赖其他镜像,从 scratch 构建,其他镜像可以以之为基础镜像进行扩展。
能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
首先先将 centos 镜像 pull 到本地
$ docker pull centos
再查看 centos 镜像大小
$ docker images centos
我们会发现,一个 CentOS 镜像居然还不到 300MB。这又是为什么呢?
Linux 操作系统由内核空间和用户空间组成。内核空间是 kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉,rootfs 被加载。用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。

不同 Linux 发行版的区别主要就是 rootfs。比如 Ubuntu 20.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。

对于 base 镜像来说,底层直接用宿主机的 kernel,自己只需要提供 rootfs 就行了。而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。
相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了,alpine 还不到 10MB。
如下,在 CentOS 7 宿主机上运行 Ubuntu 20.04 容器,看到容器的内核和宿主机的内核相同,容器复用了宿主机的内核:

如下图所示,镜像是分层存储

为什么镜像采用分层结构呢?
最大的好处就是资源共享,比如有多个镜像从同一个 base 镜像构建而来,宿主机上只需要有一份 base 镜像就可以了,多个镜像共用同一个 base 镜像。
多个容器共用同一个 base 镜像,当某个容器修改配置时,其他容器也会修改吗?
答案是不会,因为最上面的容器层是可写的,下面的其他层(镜像层)都是只读,当需要修改下层的文件时,会先复制此文件到上面的容器层,然后再修改。容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。
Docker 支持两种方法构建镜像,一是手动构建,二是通过 Dockerfile 构建。
docker run -ti --name ubuntu-vi ubuntu:20.04 bash
apt-get update && apt-get install -y vim
# 开启另一终端,提交构建的镜像
docker commit ubuntu-vi ubuntu-with-vi


# 编写Dockerfile
cat << EOF > Dockerfile
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y vim
CMD ["/bin/bash"]
EOF
# 构建镜像
docker build -t ubuntu-with-vi-dockerfile .
Docker 并不建议用户通过手动方式构建镜像。原因如下:
这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 debian base 镜像中也加入 vi,还得重复前面的所有步骤。
使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。docker history 会显示镜像的构建历史,也就是 Dockerfile 的执行过程。手工创建镜像的方式无法获取 history。
通过两种方式构建好的镜像如下:

Dockerfile 常用指令如下:
FROM 指定基础镜像
MAINTAINER 设置镜像维护者
COPY 将文件从构建上下文复制到镜像
ADD 同 COPY,如果源文件是压缩文件自动解压缩
ENV 设置环境变量
EXPOSE 指定容器监听的端口
VOLUME 定义匿名卷
WORKDIR 为 RUN、CMD、ENTRYPOINT 等命令设置工作目录
RUN 在容器中运行指定命令
CMD 指定容器启动时运行的命令,可被替换
ENTRYPOINT 指定容器启动时运行的命令
HEALTHCHECK 健康检查我们通常使用 RUN 指令安装应用和软件包,构建镜像。
如果 Docker 镜像的用途是运行应用程序或服务,比如运行一个 MySQL,应该优先使用 Exec 格式的 ENTRYPOINT 指令。
CMD 可为 ENTRYPOINT 提供额外的默认参数,同时可利用 docker run 命令行替换默认参数。
如果想为容器设置默认的启动命令,可使用 CMD 指令。
用户可在 docker run 命令行中替换此默认命令。
编译好的镜像只能在本地使用,如果想给其他人使用,最好是将容器上传到镜像仓库。容器仓库又分为公共仓库和私有仓库,hub.docker.com 是 Docker 公司提供的公有仓库,所有用户均可拉取公有仓库中的镜像,为了安全起见,通常公司会搭建自己的私有仓库。
如下是容器镜像的生命周期,包括镜像的构建、镜像打标签、推送到镜像仓库、从镜像仓库拉取镜像、镜像导入、导出、镜像删除等操作:

docker images 显示镜像列表
docker history 显示镜像构建历史
docker commit 从容器创建新镜像
docker build 从 Dockerfile 构建镜像
docker tag 给镜像打 tag
docker pull 从 registry 下载镜像
docker push 将镜像上传到 registry
docker rmi 删除 Docker host 中的镜像
docker search 搜索 Docker Hub 中的镜像