01 · 容器与镜像(Container and Image)

📅 预计 60 分钟 | ⭐ = 重要知识点 | 📌 中英术语见文末


1.0 先给直觉:集装箱的启发

先看一个老问题:你的代码在自己电脑上跑得好好的,发到服务器上就"水土不服"——不是缺库,就是版本对不上,折腾半天。为什么?

因为你的程序不是孤零零的代码,它还需要运行时、依赖库、配置文件,这些"行李"每个环境都不一样。

集装箱运输给了软件界一个答案:把货物连同打包用的箱子一起搬走,不管在哪个码头都能原样卸下。容器(container)就是这个思路——把应用和它需要的全部"行李"装进一个标准化的箱子里,到哪台机器都能直接运行。

💡 记忆口诀:容器 = 带全套行李的集装箱,应用 = 货物,代码 + 运行时 + 依赖 = 行李。


1.1 容器:一个"装好行李"的隔离环境

容器到底是什么

容器是一个隔离的、可运行的应用环境。它和普通进程一样跑在操作系统上,但拥有自己独立的文件系统、进程空间和网络,就像集装箱里的货物——装在一个独立空间里,和别的货不混。

先亲手感受一下,装好 Docker 后执行:

docker run hello-world

如果一切正常,会输出一大段英文提示,核心意思是:你的 Docker 安装正确,容器成功创建并运行了。这个 hello-world 镜像极小,只有一个打印语句,是验证环境的最佳"开关"。

容器三大特征 ⭐:

  • 隔离:容器有自己的文件系统、进程、网络,彼此看不见、不干扰。
  • 可移植:构建一次镜像,到任何装了 Docker 的机器上都能跑,"构建一次,到处运行"。
  • 轻量:所有容器共享宿主机的操作系统内核,启动只需秒级,占用的资源很小。

再看一个真实的应用容器。Nginx 是一个网页服务器,拉下来直接跑:

docker pull nginx:1.25          # 下载 Nginx 镜像
docker run -d -p 8080:80 --name web nginx:1.25   # 后台运行,映射端口
curl http://localhost:8080      # 本机访问,返回 Nginx 默认页面

这三条命令是 Docker 世界里出现频率最高的"三连":pull 取镜像、run 启动容器、curl 验证效果。容器里的 Nginx 版本、配置和你在别处拉到的完全一致——这就是"环境一致"最直观的体现。

容器怎么"活":主进程决定生死

容器不是一台常开的机器,它更像一个"前台进程":容器里有没有在跑的主进程,决定了容器的死活。当主进程退出,容器立刻停止。

比如 docker run hello-world 输出完就退出,是因为它的主进程就是打印一句话,打完任务完成,容器随之结束。这和第二讲里 docker run 常见参数(如 -d 后台运行)直接相关。

⚠️ 常见错误

  1. 把容器当虚拟机:容器共享宿主机内核,没有自己独立的操作系统,这是和虚拟机最本质的区别(见 1.4)。
  2. 以为容器会一直运行:容器主进程一退出,容器就变 exited 状态,不是设计成"永远开着"的东西。
  3. 以为容器里能装任意东西:容器用镜像创建,镜像里没有的东西(如某个系统工具)默认不存在,需要在构建或运行时安装。

1.2 镜像与分层:容器的"模板"

镜像 = 只读模板

镜像(image)是一个只读的模板,相当于刻好的印章;容器是用这个印章盖出来的一个个成品。一个印章能盖无数份,一个镜像也能启动无数个容器,彼此互不影响。

docker images          # 查看本机已有的镜像列表
docker pull nginx:1.25 # 从镜像仓库拉取 nginx 的 1.25 版本镜像

docker images 会列出镜像名、标签(tag)、镜像 ID、大小等信息。镜像的 ID 是一串十六进制,通常我们用人名(仓库名)+ 标签来指代,如 nginx:1.25

分层存储:镜像为什么"省"

一个镜像不是一块铁板,而是由若干只读层(layer)堆叠而成。每一层对应镜像构建过程中的一个步骤,层与层之间用联合文件系统(UnionFS)拼成一个完整视图。

它的好处 ⭐:

  • 复用:多个镜像往往共享底层(比如都基于 Ubuntu),这些公共层只需存一份。
  • 省流量docker pull 时只需下载本地没有的层,下次拉相似镜像会快很多。
  • 写时复制(Copy-on-Write, CoW):容器基于镜像运行后,会在最上层加一个可写的容器层。对容器里文件的任何修改都发生在这一层,镜像层永远不被改动。

分层的具体证据,可以用 docker history 查看一个镜像由哪些层组成:

docker history nginx:1.25
# IMAGE          CREATED       CREATED BY                                      SIZE
# a6bd71f48f68   2 weeks ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon off;"]   0B
# ...

每一行对应 Dockerfile 里的一条指令(第 4 讲),SIZE 为 0B 的往往是 CMD、EXPOSE 这类"只写元数据"的层。这也解释了为什么"层"既是技术细节,也是镜像构建的思考单位。

形象地看,一个镜像可以拆成下面几层:

┌──────────────────┐   容器层(可写,运行时的修改都在这里)
├──────────────────┤   应用层(如 app 源码、依赖)
├──────────────────┤   运行层(如 Python、Node 运行时)
├──────────────────┤   操作系统层(如 debian、alpine)
└──────────────────┘

⚠️ 常见错误

  1. 镜像和容器傻傻分不清:镜像是静态模板(只读),容器是运行实例(可写),关系类似"类"和"对象"。
  2. 以为修改容器会改动镜像:所有修改都写在容器层,删掉容器,改动就没了;想固化改动要把容器提交成新镜像(docker commit,一般不建议,应改 Dockerfile,见第 4 讲)。
  3. 以为一个容器只能用一个镜像:一个容器一个镜像,但一个镜像可以同时运行多个容器。

1.3 Docker 架构:谁在干活

三个角色分工

Docker 采用经典的 Client / Daemon 架构 ⭐:

角色 名称 职责
客户端 docker 命令 接收你的指令,转发给守护进程
守护进程 dockerd 真正干活的进程,管理镜像、容器、网络
镜像仓库 Registry(如 Docker Hub) 存放和分发镜像的"仓库"

用一个比喻串起来:你点外卖(client 下命令)→ 餐厅厨房(daemon 做菜)→ 食材市场(registry 供原料)

一条命令的完整旅程:

docker pull nginx        # 你在终端输入
        ↓
dockerd 收到指令         # 守护进程真正执行
        ↓
去 Docker Hub 拉取镜像   # registry 提供源
        ↓
镜像存到本地             # 下次直接用,不用再拉

执行 docker run nginx 时,daemon 会先检查本地有没有镜像,没有就自动去 registry 拉,然后再创建并启动容器。

三个概念的关系

这一串是贯穿全课程的主链路,务必背下来:

Dockerfile(菜谱)
     ↓  docker build 构建
镜像(只读模板)
     ↓  docker run 运行
容器(运行实例)

⚠️ 常见错误

  1. 以为 docker 命令直接操作容器:所有命令都是发给守护进程,由它代为执行。
  2. 混淆 Dockerfile / 镜像 / 容器:Dockerfile 是"怎么做"的菜谱,镜像是"做好的成品",容器是"端上桌的那一份"。
  3. 以为镜像存在容器里:镜像存在本机磁盘上,由 daemon 统一管理,容器只是基于它临时创建。

1.4 容器与虚拟机对比:公寓单间 vs 整套房子

隔离的层次完全不同

虚拟机(VM)用 Hypervisor 虚拟化硬件,每个虚拟机里装一个完整的操作系统(Guest OS),像一栋楼里的整套房子:有独立的厨房、卫生间、水电表。

容器共享宿主机的操作系统内核,只隔离文件系统、进程、网络等用户态资源,像一栋楼里的公寓单间:公共水电(共享内核),但房间(文件系统、进程)各自独立。

对比项 虚拟机(VM) 容器
每个实例是否带操作系统 带完整的 Guest OS 不带,共享宿主机内核
启动速度 分钟级 秒级
镜像/实例体积 GB 级 MB 级
资源开销 每个实例占较多内存 只多进程级开销,几乎无损耗
隔离强度 强(硬件级虚拟化) 较弱(内核级隔离)
可移植性 依赖 Hypervisor,较重 构建一次到处运行,非常轻

为什么云时代选容器

  • 环境一致:开发、测试、生产用同一个镜像,告别"在我机器上是好的"。
  • 快速交付与扩容:秒级启动,需要几个副本就 run 几个,弹性伸缩很容易。
  • 契合微服务:一个服务一个容器,独立部署、独立更新,互不牵连。

举个具体的场景:一个网站由前端、后端 API、数据库、消息队列四个部分组成。虚拟机方案要起四台"整套房子",每台都得装系统、配环境,占地又慢;容器方案则是四个"单间"——只带各自的运行时和依赖,共用一台宿主机的内核,秒级同时拉起。哪个更省钱、更好管理,一目了然。

一句话记住分工:虚拟机解决"跑什么"的隔离,容器解决"怎么快速一致地交付运行",两者在很多架构里其实是配合使用的,而不是二选一。

⚠️ 常见错误

  1. 以为容器比虚拟机更安全:容器共享内核,隔离主要靠内核机制(namespace/cgroup),其安全性弱于硬件级虚拟化的虚拟机。需要强隔离的陌生代码,优先考虑虚拟机或专门的沙箱。
  2. 以为容器内能运行任何操作系统:容器共享宿主机内核,不能运行和宿主内核不兼容的场景(比如在 Linux 宿主机上直接跑 Windows 容器)。
  3. 用"体积小"替代一切:容器体积小、启动快是优势,但它解决的是"打包与运行一致",不是"隔离与安全"的终极答案,两者定位不同。

📌 双语术语表(本讲)

中文 English 记忆点
容器 container 带全套行李的箱子
镜像 image 只读模板
layer 镜像的积木
联合文件系统 UnionFS 把层拼成完整视图
写时复制 Copy-on-Write (CoW) 修改发生在容器层
守护进程 daemon 真正干活的 dockerd
客户端 client 发指令的 docker 命令
镜像仓库 registry Docker Hub 等
虚拟机 virtual machine 带完整系统
隔离 isolation 互不干扰
可移植性 portability 构建一次到处运行
命名空间 namespace 隔离进程/文件系统
控制组 cgroup 限制 CPU/内存
仓库名 repository 镜像的库名
标签 tag 镜像版本号

⭐ 本讲考点清单

  1. 容器三特征:隔离、可移植、轻量(共享内核)
  2. 镜像只读、容器可写;镜像 = 模板,容器 = 实例
  3. 分层存储 + 写时复制:修改只在容器层,镜像不变
  4. Docker 架构:client → daemon → registry
  5. Dockerfile → 镜像 → 容器 的构建运行链路
  6. 虚拟机带完整 Guest OS、分钟级、GB 级、强隔离;容器共享内核、秒级、MB 级、弱隔离
  7. 容器主进程退出,容器即停止
  8. 一个镜像可启动多个容器,互不影响
  9. docker pulldocker runcurl 验证是入门三连
  10. docker history 能查看镜像的分层结构
  11. 容器隔离靠 namespace + cgroup,弱于虚拟机硬件级隔离
  12. 虚拟机解决隔离,容器解决快速一致的交付运行,常配合使用