Docker-01 基础概念与架构:容器究竟是什么
1. Docker 解决了什么问题
传统部署经常把一台机器配置成“手工艺术品”:操作系统版本、运行时、系统库、环境变量、配置文件和启动脚本都依赖人工操作。开发环境能运行,不代表测试和生产环境也能运行。
Docker 把应用及其运行时依赖封装成一个可分发的镜像:
源代码 + 依赖 + 运行时配置
↓ docker build
OCI 镜像
↓ docker run
隔离的进程
这里的“隔离”不是再启动一台完整虚拟机,而是让普通 Linux 进程看到一组受限的系统视图,并对它能使用的资源设上限。容器本质上仍是宿主机内核管理的进程。
1.1 容器和虚拟机的区别
| 对比项 | 虚拟机 | 容器 |
|---|---|---|
| 隔离边界 | 虚拟硬件和 Guest Kernel | Linux namespace、cgroup、LSM 等 |
| 是否包含内核 | 每台 VM 有自己的内核 | 与宿主机共享内核 |
| 启动速度 | 通常秒到分钟 | 通常毫秒到秒 |
| 镜像体积 | 常见为 GB 级 | 常见为 MB~数百 MB |
| 密度 | 相对较低 | 相对较高 |
| 隔离强度 | 通常更强 | 共享内核,边界取决于配置和内核 |
| 适合场景 | 不同内核、强隔离、完整 OS | 应用交付、微服务、CI、批处理 |
容器并不等于“更安全的虚拟机”。如果给容器 --privileged、挂载宿主根目录或暴露 Docker socket,隔离边界会明显变弱;高隔离场景可以使用虚拟机、microVM 或沙箱运行时。
2. 需要区分的几个对象
2.1 镜像(Image)
镜像是不可变的、分层的文件系统和运行元数据。一个镜像通常由多个只读 layer 组成:
应用层 sha256:app
依赖层 sha256:deps
基础系统层 sha256:base
多个镜像可以共享相同的基础层,因此“拉取十个镜像”不一定会占十份完整磁盘空间。镜像的身份由 digest(内容摘要)确定,例如 repo/app@sha256:...;tag 只是一个可移动的名字,生产部署不应只依赖 latest。
2.2 容器(Container)
容器是镜像的一个运行实例。运行时会在只读镜像层之上添加一个可写层,并创建进程、网络、挂载点、cgroup 和日志等运行状态。
只读镜像层(多个)
+
容器可写层(临时)
=
容器根文件系统视图
容器可写层适合临时状态,不适合数据库数据。容器删除后,写在可写层里的内容通常也会消失;持久化数据应该放到 volume 或 bind mount。
2.3 Registry、Repository、Tag、Digest
- Registry:镜像仓库服务,例如 Docker Hub、Harbor、云厂商镜像仓库。
- Repository:仓库中的镜像名称,例如
library/nginx。 - Tag:版本标签,例如
1.27-alpine;同一个 tag 以后可能指向不同 digest。 - Digest:内容寻址的不可变摘要,适合发布和审计。
拉取镜像时,客户端先解析 tag,再根据 manifest 找到不同架构的 image index 和各层 blob。多架构镜像可以让同一个 tag 在 amd64、arm64 上选择不同的层。
3. Docker Engine 的组成
日常所说的“Docker”其实包含多个组件:
docker CLI ── HTTP/Unix socket ── dockerd
│
└── containerd
│
├── containerd-shim
└── runc ── Linux kernel
3.1 Docker CLI
docker 是客户端,不负责直接创建进程。它把 docker run、docker ps 等命令转换成 Docker Engine API 请求。客户端可以连接本机 Unix socket,也可以通过 context 连接远程 daemon。
docker context ls
docker version
docker info
3.2 dockerd
dockerd 是 Docker Engine 的 API 服务和控制面,负责镜像、容器、网络、卷等对象的管理,并把容器生命周期交给 containerd。Linux 上默认的本地 socket 通常是 /var/run/docker.sock。
把 socket 挂进另一个容器,往往等价于给它 Docker daemon 的高权限控制权;因此不要把 /var/run/docker.sock 当成普通配置文件使用。
3.3 containerd、shim 和 runc
- containerd:负责镜像传输、解包、容器生命周期和 snapshot 管理。
- runc:遵循 OCI Runtime Specification 的低层运行时,根据 config.json 创建 namespace、挂载 rootfs、设置 capability 和 cgroup,然后启动容器进程。
- containerd-shim:让容器进程不必直接依赖 containerd 主进程;daemon 重启时,已有容器可以继续运行。
Docker Engine 是一种产品形态,containerd 和 runc 是更底层的通用组件。Kubernetes 也可以直接使用 containerd 等符合 CRI 的运行时,不代表必须安装 Docker CLI。
4. OCI 为什么重要
OCI(Open Container Initiative)定义了三类关键规范:
- Image Specification:镜像 manifest、config、layer 的格式。
- Runtime Specification:运行时如何根据 bundle 和
config.json创建容器。 - Distribution Specification:镜像如何在 registry 上传、下载和校验。
因此,Docker 构建的镜像通常可以被 containerd、Podman、Kubernetes 等工具使用;“Docker 镜像”更多是习惯称呼,实际格式通常是 OCI 兼容格式。
5. 一次 docker run 发生了什么
docker run --name web -d -p 8080:80 nginx:1.27-alpine
可以拆成以下步骤:
- CLI 解析参数,并向 dockerd 发起创建/启动请求。
- dockerd 检查本地是否有
nginx:1.27-alpine;没有则从 registry 拉取 manifest 和 layer。 - containerd 解包镜像,交给 snapshotter(常见是 overlayfs)准备 rootfs。
- dockerd 创建容器元数据、网络端点、端口映射和日志配置。
- containerd 创建任务,准备 OCI bundle 和
config.json。 - shim 调用 runc;runc 创建 namespace、挂载 rootfs、应用 cgroup/capability/seccomp,然后执行镜像的 entrypoint。
- 容器主进程成为该 PID namespace 中的 PID 1,退出后容器状态变为
exited。
端口映射不是“把端口写进镜像”,而是在宿主机网络栈上配置转发规则。镜像只描述应用可能监听的端口,真正的映射发生在运行时。
6. 先建立正确的心智模型
6.1 镜像不是虚拟机快照
镜像没有独立内核,也不会保存一个正在运行的进程状态。它是文件系统层和元数据;容器启动时,运行时才创建进程和隔离环境。
6.2 容器不是轻量级服务器
容器适合一个主要服务进程。它可以包含多个进程,但要明确 PID 1 的信号处理、子进程回收、日志和健康检查责任。用一个 shell 启动多个后台服务,往往会导致 docker stop 时信号传不到真正的应用。
6.3 Docker 不负责所有编排问题
Docker Engine 负责单机上的镜像、容器、网络和卷。多机调度、服务发现、滚动发布、自动修复和扩缩容属于 Compose、Swarm、Kubernetes 或其他平台的职责。
7. 常见误区
- “容器里 root 就是安全的 root”:容器 root 可能仍映射到宿主 UID 0,需配合 rootless、user namespace 和 capability 限制。
- “EXPOSE 会开放端口”:
EXPOSE只是镜像元数据;对外访问通常需要-p或 Compose 的ports。 - “重启容器数据还在”:如果数据写在容器可写层,删除/重建容器后不保证存在;持久化请使用 volume/bind mount。
- “镜像 tag 就是版本”:tag 可被覆盖,生产发布应记录 digest。
- “Docker daemon 重启容器都会退出”:现代 containerd/shim 设计下,已有任务通常可以继续运行,但具体行为受 daemon 配置和运行时影响。
8. 动手验证:不要只看架构图
8.1 从镜像到容器逐层观察
docker pull nginx:1.27-alpine
docker image inspect nginx:1.27-alpine | jq '.[0] | {
id: .Id,
architecture: .Architecture,
entrypoint: .Config.Entrypoint,
cmd: .Config.Cmd,
exposed: .Config.ExposedPorts,
layers: .RootFS.Layers
}'
docker run -d --name arch-lab -p 127.0.0.1:18080:80 nginx:1.27-alpine
docker inspect arch-lab | jq '.[0] | {
id: .Id,
pid: .State.Pid,
status: .State.Status,
mounts: .Mounts,
networks: .NetworkSettings.Networks,
restart: .HostConfig.RestartPolicy
}'
在 Linux 主机上继续观察同一个进程的两个视图:
PID=$(docker inspect -f '{{.State.Pid}}' arch-lab)
ps -o pid,ppid,user,comm,args -p "$PID"
sudo nsenter -t "$PID" -p -m -n ps -ef
sudo nsenter -t "$PID" -n ip addr
sudo nsenter -t "$PID" -m findmnt -R /
宿主机可能把 Nginx 显示为 PID 数千,而容器内显示为 PID 1;容器内有自己的 eth0、路由和挂载视图。这是 namespace 的实际效果,不是启动了第二个 Linux 内核。
8.2 观察 registry 到 layer 的关系
docker image ls --digests nginx
docker history --no-trunc nginx:1.27-alpine
docker system df -v
history 展示构建历史,RootFS.Layers 展示当前镜像引用的 layer,digest 才是内容身份。tag 可以被仓库管理员移动,所以部署记录应至少保存:仓库地址、tag、digest、架构、构建 commit 和基础镜像版本。
8.3 远程 context 为什么改变安全边界
docker context ls
docker context show
docker -H unix:///var/run/docker.sock version
CLI 只是 API 客户端。切换 context 后,同一个 docker rm 可能作用在另一台机器;CI 和生产脚本应显式打印 context、主机名和镜像 digest。远程 daemon 不应直接暴露未加密的 2375 端口,优先使用 SSH 或双向 TLS。
实验结束清理:
docker rm -f arch-lab
9. 对象、状态和持久化边界
| 对象 | 创建来源 | 是否可变 | 删除容器后是否保留 |
|---|---|---|---|
| Image | build/pull/load |
layer 不可变,tag 可移动 | 保留,除非显式删除且无引用 |
| Container | run/create |
状态和可写层可变 | 容器可写层删除,镜像保留 |
| Network | network create/Compose |
endpoint 会变化 | 与容器/项目生命周期有关 |
| Named volume | volume create/Compose |
数据可变 | 默认保留,down -v 可能删除 |
| Registry artifact | push |
tag 可移动,digest 不变 | 与本地容器无关 |
这个边界解释了很多事故:删除容器不等于删除镜像,也不等于删除 volume;重启容器不等于重建容器;重建容器如果没有挂载 volume,就可能丢掉可写层数据。
10. 本篇小结
Docker 的核心价值是标准化应用交付,而不是提供一台小型虚拟机。记住这条链路:
镜像(不可变文件层)
→ containerd(生命周期)
→ runc(创建隔离进程)
→ namespace/cgroup/overlayfs(内核机制)
→ 容器主进程
后面的安装、命令、网络和存储,都是在操作这条链路上的不同对象。
| 上一篇 | 下一篇 |
|---|---|
| 00-学习路线与实验环境 | 02-安装与配置 |