容器是现代基础设施的构建单元——它是 Kubernetes 编排的对象、CI 流水线构建的产物、微服务发布的方式。Docker 把"把应用连同它运行所需的一切打包"变成一条命令的工作流,从而让容器普及。最关键要理解的是:容器不是一台轻量虚拟机——它是共享内核上的一个隔离进程,这个区别解释了它的速度,也解释了它的局限。
- 容器把应用 + 它所有依赖打包成一个可移植镜像,在任何主机上运行结果一致——终结"在我机器上是好的"。
- 它是隔离进程,不是 VM——Linux namespaces 给它自己的系统视图;cgroups 限制它的 CPU/内存。它共享主机内核。
- 对比 VM:没有客机操作系统,所以容器是 MB 级而非 GB 级、毫秒级启动、可高密度部署——代价是隔离更弱。
- 镜像是分层且只读的——每条 Dockerfile 指令是一个可缓存的层;层在镜像间共享以省空间和构建时间。
- Dockerfile 是可复现的构建配方;registry(Docker Hub、ECR)存储并分发镜像。
- 容器是临时且不可变的——状态放在 volume/外部存储里;你替换而非修补一个运行中的容器。
容器把应用和它的依赖打包成一个不可变镜像,到处运行结果都一样。底层它只是一个被 Linux namespaces(自己的文件系统、网络、PID 空间)隔离、被 cgroups(CPU/内存上限)约束的主机进程,共享主机内核——这就是它远比 VM 轻的原因。镜像由 Dockerfile 构建成可缓存、可共享的层,经 registry 分发。让容器保持无状态且不可变,你就得到可移植、可回滚、能被 Kubernetes 管理的部署。
问题:"在我机器上是好的"
有容器之前,部署软件意味着在每台机器上复现它的环境——对的语言运行时、库版本、系统包、配置。开发者笔记本、CI、生产之间的细微差异造成了那句臭名昭著的"可在我机器上是好的"。容器解决这个,靠把应用连同它整个用户态环境打包成一个产物,所以你测的东西和你在生产跑的东西逐字节相同。
容器到底是什么
容器是一个普通的 Linux 进程,只是被赋予了对系统的隔离视图,用到两个内核特性:
- namespaces 提供隔离——容器拿到自己的文件系统挂载、网络栈、进程 ID(PID namespace,所以它的"PID 1"是它的主进程)、主机名和用户。它看不到主机的其他进程或文件。
- cgroups(控制组) 提供资源限制——限定容器能用多少 CPU、内存、I/O,这样一个容器不能饿死其他容器。
关键是,主机上所有容器共享那台主机的内核。容器里没有客机操作系统——只有你的应用和它的用户态文件,作为隔离、受限的主机进程运行。这一个事实是容器对比 VM 一切取舍的根源。
容器对比虚拟机
| 方面 | 容器 | 虚拟机 |
|---|---|---|
| 隔离单元 | 进程(namespaces + cgroups) | hypervisor 上的完整客机 OS |
| 内核 | 共享主机内核 | 每个 VM 有自己的内核 |
| 体积 | MB 级 | GB 级 |
| 启动 | 毫秒级 | 数秒到数分钟 |
| 密度 | 每主机数百个 | 每主机几个 |
| 隔离强度 | 较弱(共享内核) | 较强(硬件级) |
结论:容器在轻量、速度、密度上胜出(很适合大量小服务),而 VM 在隔离上胜出(更强的安全边界)。实践中两者常分层——云上容器常跑在 VM 里,以兼得两者。
镜像与分层
容器镜像(image)是实例运行所基于的、不可变、打包好的文件系统 + 元数据。它的定义性特征是分层(layer)构建:每层是一个只读的文件系统差异,通过联合文件系统(union filesystem)叠成最终的根文件系统。层是内容寻址且共享的——如果十个镜像基于同一个基础层,该层只存一份、被复用。容器运行时,顶上加一个薄的可写层(写时复制),所以底下的镜像保持不可变。
┌─────────────────────────────┐ ← 可写层(每容器一个,临时)
├─────────────────────────────┤
│ COPY app/ (层 4) │ ┐
│ RUN npm install (层 3) │ │ 只读镜像层,
│ COPY package.json(层 2) │ │ 可缓存 + 跨镜像共享
│ FROM node:20 (层 1) │ ┘
└─────────────────────────────┘
改应用代码 → 只重建层 4;层 1–3 从缓存复用
这种分层是构建快(未变的层走缓存)的原因,也是为什么 Dockerfile 指令顺序重要——把不常变的步骤(装依赖)放在常变的步骤(拷源码)之前,让缓存能扛住大多数改动。
Dockerfile
镜像由 Dockerfile 构建——一份声明式配方,每条指令产生一个层:
FROM node:20-alpine # 小的基础镜像
WORKDIR /app
COPY package*.json ./ # 先拷依赖 → 对缓存友好
RUN npm ci --omit=dev
COPY . . # 再拷源码(常变)
EXPOSE 3000
CMD ["node", "server.js"] # 容器运行的进程
多阶段构建(multi-stage build)(一个编译的构建阶段,再一个只拷产物的瘦运行阶段)把构建工具留在身后,从而让最终镜像很小——是做精简、安全镜像的关键实践。
Registry
镜像通过 registry 分发——你把构建好的镜像 push 上去、再 pull 下来(Docker Hub、AWS ECR、GitHub Container Registry 等)。镜像打标签(如 myapp:1.4.2),而因为层是内容寻址的,一次 pull 只下载主机还没有的层。registry 是你的 CI/CD 流水线(构建并推送)与编排器(拉取并运行)之间的交接点。
状态、网络与运行时
两个运维事实很重要。第一,容器的可写层是临时的——容器被删时它就没了,所以持久数据放在 volume(挂载到主机或网络存储)或外部服务(数据库、对象存储)里,绝不放在容器内。第二,每个容器有自己的网络 namespace;Docker 用虚拟网络把它们连起来、把端口映射到主机。Docker 引擎(或 containerd 等其他运行时)负责把镜像变成一个运行中的隔离进程。
容器为什么胜出
- 可移植——同一个镜像在笔记本、CI、生产上运行,结果一致。
- 密度与速度——毫秒级启动、每主机数百个,很适合微服务和自动扩缩。
- 不可变部署——你发布一个带版本的镜像,靠切标签回滚,而不是去改服务器。
- 编排的基础——统一、自包含的单元正是 Kubernetes 用来调度、扩缩、自愈所需要的。
常见坑
- 镜像臃肿——肥基础镜像和遗留的构建工具让镜像巨大、拉取慢;用精简基础镜像和多阶段构建。
- 把容器当 VM 用——别 SSH 进去改它、别跑一个带很多进程的 init;理想是每容器一个主进程。
- 状态放在容器里——任何写到容器文件系统的东西重启即丢;用 volume。
- 安全——共享内核意味着隔离更弱;别用 root 跑、扫描镜像 CVE、别把 secret 烤进层里(它们会留在镜像里)。
容器是一个共享主机内核、被隔离且资源受限的进程——不是 VM——这就是它小、快、密的原因。镜像是不可变、分层、可共享的,由 Dockerfile 可复现地构建、经 registry 分发。让它们无状态且不可变,你就得到可移植、可回滚的部署,能被 Kubernetes 这样的编排器大规模管理。
容器对比 VM?容器是共享主机内核的隔离进程(namespaces + cgroups);VM 在 hypervisor 上跑完整客机 OS。容器 MB 级、毫秒启动;VM GB 级、隔离更强。
到底是什么在隔离容器?Linux namespaces(文件系统、网络、PID 视图)加 cgroups(CPU/内存上限)——没有客机内核。
镜像分层有什么用?每条 Dockerfile 步骤是一个可缓存、内容寻址、跨镜像共享的层——构建快、存储小;把最不常变的步骤放前面。
容器状态放哪?不放在临时的可写层里——放 volume 或外部存储,因为容器不可变、可替换。
容器为何在微服务上胜出?可移植、毫秒启动、高密度、不可变带版本部署,还是编排的完美单元。