目录

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 rundocker 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)定义了三类关键规范:

  1. Image Specification:镜像 manifest、config、layer 的格式。
  2. Runtime Specification:运行时如何根据 bundle 和 config.json 创建容器。
  3. Distribution Specification:镜像如何在 registry 上传、下载和校验。

因此,Docker 构建的镜像通常可以被 containerd、Podman、Kubernetes 等工具使用;“Docker 镜像”更多是习惯称呼,实际格式通常是 OCI 兼容格式。

5. 一次 docker run 发生了什么

docker run --name web -d -p 8080:80 nginx:1.27-alpine

可以拆成以下步骤:

  1. CLI 解析参数,并向 dockerd 发起创建/启动请求。
  2. dockerd 检查本地是否有 nginx:1.27-alpine;没有则从 registry 拉取 manifest 和 layer。
  3. containerd 解包镜像,交给 snapshotter(常见是 overlayfs)准备 rootfs。
  4. dockerd 创建容器元数据、网络端点、端口映射和日志配置。
  5. containerd 创建任务,准备 OCI bundle 和 config.json
  6. shim 调用 runc;runc 创建 namespace、挂载 rootfs、应用 cgroup/capability/seccomp,然后执行镜像的 entrypoint。
  7. 容器主进程成为该 PID namespace 中的 PID 1,退出后容器状态变为 exited

端口映射不是“把端口写进镜像”,而是在宿主机网络栈上配置转发规则。镜像只描述应用可能监听的端口,真正的映射发生在运行时。

6. 先建立正确的心智模型

6.1 镜像不是虚拟机快照

镜像没有独立内核,也不会保存一个正在运行的进程状态。它是文件系统层和元数据;容器启动时,运行时才创建进程和隔离环境。

6.2 容器不是轻量级服务器

容器适合一个主要服务进程。它可以包含多个进程,但要明确 PID 1 的信号处理、子进程回收、日志和健康检查责任。用一个 shell 启动多个后台服务,往往会导致 docker stop 时信号传不到真正的应用。

6.3 Docker 不负责所有编排问题

Docker Engine 负责单机上的镜像、容器、网络和卷。多机调度、服务发现、滚动发布、自动修复和扩缩容属于 Compose、Swarm、Kubernetes 或其他平台的职责。

7. 常见误区

  1. “容器里 root 就是安全的 root”:容器 root 可能仍映射到宿主 UID 0,需配合 rootless、user namespace 和 capability 限制。
  2. “EXPOSE 会开放端口”EXPOSE 只是镜像元数据;对外访问通常需要 -p 或 Compose 的 ports
  3. “重启容器数据还在”:如果数据写在容器可写层,删除/重建容器后不保证存在;持久化请使用 volume/bind mount。
  4. “镜像 tag 就是版本”:tag 可被覆盖,生产发布应记录 digest。
  5. “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-安装与配置