mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
971 字
3 分钟
容器完整流程:docker run 背后
2026-05-14

docker run -d -p 80:80 nginx——一条命令,0.3 秒,一个运行中的 Nginx 容器。但在这 0.3 秒内,发生了什么?

Docker CLI 解析参数,通过 gRPC 调用 dockerd;dockerd 检查本地镜像,不存在则从 Registry 拉取;拉取完成后,containerd 解压镜像层到 OverlayFS;containerd 启动 containerd-shim;shim 调用 runc create;runc 创建 Namespace、配置 Cgroup、挂载 OverlayFS、设置 seccomp、执行容器进程;shim 收集容器 PID 并返回给 containerd;containerd 通知 dockerd;dockerd 配置端口映射;Docker CLI 返回容器 ID。

本章将完整追踪这条调用链的每一步,将前 8 章的知识融会贯通。

一、docker run 的完整调用链#

1.1 调用链总览#

sequenceDiagram participant CLI as Docker CLI participant DOCKERD as dockerd participant CTNRD as containerd participant SHIM as containerd-shim participant RUNC as runc participant KERNEL as Linux 内核 CLI->>DOCKERD: 1. POST /containers/create DOCKERD->>CTNRD: 2. Pull image (if needed) CTNRD->>DOCKERD: 3. Image ready DOCKERD->>CTNRD: 4. Create container CTNRD->>CTNRD: 5. Unpack image → OverlayFS CTNRD->>SHIM: 6. Start shim process SHIM->>RUNC: 7. runc create RUNC->>KERNEL: 8. clone(CLONE_NEWPID|CLONE_NEWNS|...) KERNEL-->>RUNC: 新 Namespace 中的进程 RUNC->>KERNEL: 9. mount overlayfs RUNC->>KERNEL: 10. pivot_root RUNC->>KERNEL: 11. write cgroup files RUNC->>KERNEL: 12. prctl(SECCOMP_SET_MODE_FILTER) RUNC-->>SHIM: 容器已创建 SHIM-->>CTNRD: PID + 状态 CTNRD-->>DOCKERD: 容器已创建 CLI->>DOCKERD: 13. POST /containers/{id}/start DOCKERD->>CTNRD: 14. Start task CTNRD->>SHIM: 15. runc start SHIM->>RUNC: 16. runc start RUNC->>KERNEL: 17. 执行用户命令 KERNEL-->>SHIM: 容器运行中 SHIM-->>CTNRD: 容器运行中 CTNRD-->>DOCKERD: 容器运行中 DOCKERD->>DOCKERD: 18. 配置端口映射 (iptables) DOCKERD-->>CLI: 容器 ID

1.2 涉及的进程#

进程PID 示例角色生命周期
docker CLI10000命令行客户端命令执行期间
dockerd1000Docker daemon持久运行
containerd2000容器运行时管理持久运行
containerd-shim3000容器进程监督容器运行期间
runc (create)4000OCI 运行时创建后退出
runc init4001容器初始化初始化后 exec 为用户进程
nginx4001→容器进程容器运行期间

二、阶段一:镜像拉取#

2.1 检查本地镜像#

// dockerd 检查本地镜像(简化)
func (daemon *Daemon) CreateContainer(params CreateParams) (*Container, error) {
// 1. 解析镜像名称
ref, err := reference.ParseNormalizedNamed(params.Image)
// 2. 查找本地镜像
image, err := daemon.imageStore.Get(ref)
if err != nil {
// 本地不存在,拉取镜像
image, err = daemon.PullImage(ref)
}
// 3. 创建容器配置
container, err := daemon.newContainer(image, params)
return container, nil
}

2.2 镜像拉取流程#

# docker pull nginx:latest 的底层操作
# 1. 解析镜像名称
# nginx:latest → docker.io/library/nginx:latest
# 2. 获取 Manifest
# GET https://registry-1.docker.io/v2/library/nginx/manifests/latest
# 3. 下载 Config blob
# GET https://registry-1.docker.io/v2/library/nginx/blobs/sha256:abc123...
# 4. 下载 Layer blob(按需,跳过已有层)
# GET https://registry-1.docker.io/v2/library/nginx/blobs/sha256:def456...
# 5. 解压 Layer 到 OverlayFS
# tar -xzf layer1.tar.gz -C /var/lib/docker/overlay2/l1/diff
# tar -xzf layer2.tar.gz -C /var/lib/docker/overlay2/l2/diff

2.3 镜像层解压与 OverlayFS#

# 查看 nginx 镜像的层
docker inspect nginx --format '{{json .RootFS.Layers}}' | python3 -m json.tool
# 输出示例(6 层):
# "sha256:a1b2c3d4..."
# "sha256:e5f6g7h8..."
# "sha256:i9j0k1l2..."
# "sha256:m3n4o5p6..."
# "sha256:q7r8s9t0..."
# "sha256:u1v2w3x4..."
# 每层对应 OverlayFS 的一个 lowerdir
ls /var/lib/docker/overlay2/l/
# 每个短链接指向一个层的 diff 目录

三、阶段二:容器创建#

3.1 dockerd 生成容器配置#

dockerd 将 Docker 参数转换为 OCI Runtime Spec 格式的 config.json:

// dockerd 生成容器配置(简化)
func (daemon *Daemon) createContainerSpec(container *Container) (*specs.Spec, error) {
spec := &specs.Spec{
Version: specs.Version,
Process: &specs.Process{
Args: container.Config.Entrypoint + container.Config.Cmd,
Env: container.Config.Env,
Cwd: container.Config.WorkingDir,
User: container.Config.User,
Terminal: container.Config.Tty,
},
Root: &specs.Root{
Path: "rootfs",
Readonly: container.HostConfig.ReadonlyRootfs,
},
Hostname: container.Config.Hostname,
Mounts: daemon.generateMounts(container),
Linux: &specs.Linux{
Namespaces: daemon.generateNamespaces(container),
Resources: daemon.generateResources(container),
Seccomp: daemon.generateSeccomp(container),
},
}
return spec, nil
}

3.2 Docker 参数到 OCI Spec 的映射#

Docker 参数OCI Spec 字段说明
-e FOO=barprocess.env环境变量
-u nginxprocess.user运行用户
-h myhosthostname主机名
--memory=512mlinux.resources.memory.limit内存限制
--cpus=2linux.resources.cpu.quota/periodCPU 限制
--cap-add=NET_ADMINprocess.capabilitiesLinux Capabilities
--security-opt seccomp=...linux.seccompseccomp 规则
--pid=hostlinux.namespaces (无 PID NS)共享宿主 PID NS
--network=hostlinux.namespaces (无 Network NS)共享宿主网络
--read-onlyroot.readonly只读根文件系统
-v /host:/containermounts挂载点

3.3 containerd 创建容器#

// containerd 创建容器(简化)
func (m *TaskManager) Create(ctx context.Context, id string, spec *specs.Spec) error {
// 1. 准备 rootfs 快照
mounts, err := m.snapshotter.Prepare(ctx, id, parentSnapshot)
// 这一步创建 OverlayFS 的 upperdir
// 2. 写入 config.json
if err := writeConfig(bundlePath, spec); err != nil {
return err
}
// 3. 启动 shim
shim, err := m.startShim(ctx, bundle, id)
// shim 进程启动后,通过 gRPC 与 containerd 通信
// 4. 通过 shim 创建容器
_, err = shim.Create(ctx, &task.CreateRequest{
ID: id,
Bundle: bundlePath,
Rootfs: mounts,
Terminal: spec.Process.Terminal,
})
return err
}

四、阶段三:runc 创建容器#

4.1 runc create 的系统调用#

# 用 strace 追踪 runc create 的关键系统调用
sudo strace -f -e trace=clone,unshare,mount,pivot_root,prctl,capset,write \
-o /tmp/runc-create.log runc create --bundle /tmp/bundle mycontainer
# 关键系统调用序列(简化):
# 1. clone(CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWNET)
# → 创建新 Namespace 中的子进程
# 2. mount("overlay", "/var/lib/docker/overlay2/.../merged", "overlay", ...)
# → 挂载 OverlayFS
# 3. mount("proc", "/proc", "proc", ...)
# → 挂载 procfs
# 4. pivot_root("/var/lib/docker/overlay2/.../merged", ...)
# → 切换根文件系统
# 5. write(5, "200000 100000", ..., "cpu.max")
# → 设置 CPU Cgroup
# 6. write(5, "536870912", ..., "memory.max")
# → 设置内存 Cgroup
# 7. capset(CAP_SETPCAP, {CAP_AUDIT_WRITE, CAP_KILL, ...})
# → 设置 Capabilities
# 8. prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)
# → 设置 seccomp 过滤
# 9. execve("/docker-entrypoint.sh", ["nginx", "-g", "daemon off;"], ...)
# → 执行用户命令

4.2 容器创建的完整时序#

sequenceDiagram participant SHIM as containerd-shim participant RUNC as runc participant INIT as runc init participant KERNEL as Linux 内核 SHIM->>RUNC: exec: runc create RUNC->>KERNEL: 1. clone(CLONE_NEWPID|CLONE_NEWNS|...) KERNEL->>INIT: 在新 Namespace 中启动 Note over INIT: 以下操作在新 Namespace 中 INIT->>KERNEL: 2. mount("overlay", merged, "overlay", lowerdir=...:upperdir=...) INIT->>KERNEL: 3. mount("proc", "/proc", "proc", 0, "") INIT->>KERNEL: 4. mount("sysfs", "/sys", "sysfs", MS_RDONLY, "") INIT->>KERNEL: 5. mount("devtmpfs", "/dev", "devtmpfs", 0, "") INIT->>KERNEL: 6. mount("tmpfs", "/dev/shm", "tmpfs", 0, "") INIT->>KERNEL: 7. mount("tmpfs", "/run", "tmpfs", 0, "") INIT->>KERNEL: 8. mount("cgroup2", "/sys/fs/cgroup", "cgroup2", 0, "") INIT->>KERNEL: 9. pivot_root(merged, put_old) Note over INIT: 根文件系统切换完成 INIT->>KERNEL: 10. sethostname("mycontainer") INIT->>KERNEL: 11. write("200000 100000" → cpu.max) INIT->>KERNEL: 12. write("536870912" → memory.max) INIT->>KERNEL: 13. write(PID → cgroup.procs) INIT->>KERNEL: 14. capset(bounding={CAP_AUDIT_WRITE, CAP_KILL, ...}) INIT->>KERNEL: 15. prctl(PR_SET_NO_NEW_PRIVS, 1) INIT->>KERNEL: 16. prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, bpf_prog) INIT->>KERNEL: 17. setuid(101) / setgid(101) INIT->>KERNEL: 18. execve("/docker-entrypoint.sh", args, env) Note over KERNEL: 容器进程开始运行 RUNC-->>SHIM: 容器已创建,PID = 4001

五、阶段四:容器启动#

5.1 runc start#

# runc start 发送信号让 runc init 执行用户命令
runc start mycontainer
# 底层操作:
# 1. 通过管道通知 runc init 执行用户命令
# 2. runc init 调用 execve() 替换为用户进程
# 3. 容器进程开始运行

5.2 端口映射#

# dockerd 配置端口映射(-p 80:80)
# 1. 创建 iptables DNAT 规则
sudo iptables -t nat -A DOCKER -p tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
# 2. 创建 iptables MASQUERADE 规则
sudo iptables -t nat -A POSTROUTING -s 172.17.0.2 -j MASQUERADE
# 3. 创建 iptables ALLOW 规则
sudo iptables -A DOCKER -d 172.17.0.2 -p tcp --dport 80 -j ACCEPT
# 查看 Docker 的 iptables 规则
sudo iptables -t nat -L DOCKER -n -v

5.3 网络配置#

# Docker 的网络配置流程
# 1. 创建 veth pair
sudo ip link add veth123 type veth peer name veth456
# 2. 将一端放入容器 Network Namespace
sudo ip link set veth456 netns <container-pid>
# 3. 将另一端连接到 docker0 bridge
sudo ip link set veth123 master docker0
# 4. 在容器内配置 IP 地址
sudo nsenter -t <container-pid> -n ip addr add 172.17.0.2/16 dev eth0
# 5. 在容器内设置默认路由
sudo nsenter -t <container-pid> -n ip route add default via 172.17.0.1

六、阶段五:容器运行#

6.1 容器运行时的进程与 Namespace 关系#

容器启动后,宿主机上可以看到完整的进程和 Namespace 映射:

graph TB subgraph 宿主机["宿主机视角"] SYSTEMD["systemd<br/>PID 1"] DOCKERD["dockerd<br/>PID 1000"] CTNRD["containerd<br/>PID 2000"] SHIM["containerd-shim<br/>PID 3000"] NGINX["nginx<br/>PID 4001 (宿主 PID)"] end subgraph 容器内["容器内视角"] PID1["PID 1 (nginx master)"] PID2["PID 10 (nginx worker)"] end SYSTEMD --> DOCKERD --> CTNRD --> SHIM --> NGINX NGINX -.->|"PID Namespace 映射"| PID1 NGINX -.->|"PID Namespace 映射"| PID2 subgraph NS隔离["Namespace 隔离"] NET["Network NS: 172.17.0.2"] MNT["Mount NS: OverlayFS merged"] UTS["UTS NS: hostname=mycontainer"] IPC["IPC NS: 独立消息队列"] CG["Cgroup NS: /docker/abc123"] end style 宿主机 fill:#e8eaf6,stroke:#283593 style 容器内 fill:#e0f2f1,stroke:#00695c style NS隔离 fill:#fff3e0,stroke:#e65100

6.2 容器运行时的状态#

# 查看容器的完整状态
docker inspect mynginx | python3 -m json.tool
# 关键信息:
# - State.Pid: 容器进程在宿主机上的 PID
# - State.Running: 是否运行中
# - NetworkSettings.IPAddress: 容器 IP
# - HostConfig.PortBindings: 端口映射
# - HostConfig.Memory: 内存限制
# - HostConfig.NanoCpus: CPU 限制

6.3 容器进程的 Namespace#

# 查看容器进程的所有 Namespace
PID=$(docker inspect -f '{{.State.Pid}}' mynginx)
ls -la /proc/$PID/ns/
# 输出:
# cgroup -> 'cgroup:[4026532700]' ← 独立 Cgroup NS
# ipc -> 'ipc:[4026532698]' ← 独立 IPC NS
# mnt -> 'mnt:[4026532696]' ← 独立 Mount NS
# net -> 'net:[4026532699]' ← 独立 Network NS
# pid -> 'pid:[4026532697]' ← 独立 PID NS
# user -> 'user:[4026531837]' ← 共享宿主 User NS
# uts -> 'uts:[4026532695]' ← 独立 UTS NS

6.4 容器进程的 Cgroup#

# 查看容器的 Cgroup 路径
cat /proc/$PID/cgroup
# 输出(Cgroup v2):
# 0::/system.slice/docker-abc123.scope
# 查看容器的资源限制
cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.max
cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-abc123.scope/pids.max

七、完整流程的代码追踪#

7.1 用 Go 模拟 docker run#

package main
import (
"context"
"fmt"
"log"
"os"
"syscall"
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/pkg/namespaces"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func main() {
ctx := namespaces.WithNamespace(context.Background(), "default")
// 1. 连接 containerd
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 2. 拉取镜像
fmt.Println("Pulling image...")
image, err := client.Pull(ctx, "docker.io/library/nginx:alpine",
containerd.WithPullUnpack)
if err != nil {
log.Fatal(err)
}
// 3. 创建容器
fmt.Println("Creating container...")
container, err := client.NewContainer(ctx, "my-nginx",
containerd.WithImage(image),
containerd.WithNewSnapshot("nginx-snapshot", image),
containerd.WithNewSpec(
containerd.WithImageConfig(image),
withResourceLimits(),
),
)
if err != nil {
log.Fatal(err)
}
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
// 4. 创建并启动任务
fmt.Println("Starting container...")
task, err := container.NewTask(ctx,
containerd.NewIO(os.Stdin, os.Stdout, os.Stderr))
if err != nil {
log.Fatal(err)
}
defer task.Delete(ctx)
if err := task.Start(ctx); err != nil {
log.Fatal(err)
}
// 5. 等待退出
statusC, err := task.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Container running with PID %d\n", task.Pid())
status := <-statusC
fmt.Printf("Container exited with code %d\n", status.ExitCode())
}
func withResourceLimits() containerd.SpecOpts {
return func(_ context.Context, _ *containerd.Client, c *specs.Spec) error {
c.Linux.Resources = &specs.LinuxResources{
Memory: &specs.LinuxMemory{Limit: int64Ptr(512 * 1024 * 1024)},
CPU: &specs.LinuxCPU{
Quota: int64Ptr(200000),
Period: uint64Ptr(100000),
},
}
return nil
}
}
func int64Ptr(v int64) *int64 { return &v }
func uint64Ptr(v uint64) *uint64 { return &v }

八、性能分析#

8.1 docker run 各阶段耗时#

gantt title docker run 各阶段耗时(镜像已存在) dateFormat X axisFormat %Lms section OCI Bundle 生成 config.json :0, 5 section 运行时 shim 启动 (fork+exec) :5, 25 runc create (NS+Cgroup+mount) :25, 125 runc start (execve) :125, 130 section 网络 veth/bridge/iptables :130, 180 section 总计 容器就绪 :milestone, 180
阶段典型耗时说明
镜像拉取1-60s取决于网络和镜像大小
镜像解压0.1-5s取决于层数和大小
OCI Bundle 生成1-5ms生成 config.json
shim 启动5-20msfork + exec
runc create20-100msNamespace/Cgroup/mount
runc start1-5ms执行用户命令
网络配置5-50msveth/bridge/iptables
总计(镜像已存在)50-200ms

8.2 优化建议#

# 1. 使用更小的基础镜像
docker run -d alpine-based-image # 比 Ubuntu 镜像快 5-10x
# 2. 预拉取镜像
docker pull nginx:latest # 提前拉取,避免运行时等待
# 3. 使用 --init 避免 PID 1 问题
docker run --init nginx # 使用 tini 作为 PID 1
# 4. 使用 host 网络模式(跳过网络配置)
docker run --network=host nginx # 省去 veth/bridge 配置
# 5. 限制日志大小
docker run --log-driver=json-file --log-opt max-size=10m nginx

九、本章小结#

上一章深入解读了containerd-shim 的解耦机制的内部机制。

阶段关键操作涉及组件
镜像拉取Registry API → Content Store → Snapshotcontainerd
容器创建生成 OCI Bundle → 启动 shimcontainerd
runc createclone → mount → pivot_root → cgroup → seccomprunc + 内核
runc startexecve 用户命令runc
网络配置veth pair → bridge → iptablesdockerd
容器运行shim 监督 → 事件通知shim + containerd
Note

理解 docker run 的完整流程,是排查容器问题的关键。当你遇到容器启动失败、网络不通、资源限制不生效等问题时,可以按这条调用链逐步定位——是镜像问题?是 runc 问题?还是内核问题?

Tip

排查容器启动失败的常用方法:docker logs 查看容器日志,docker inspect 查看容器状态和退出码,journalctl -u containerd 查看 containerd 日志,dmesg 查看内核日志(OOM、seccomp 拒绝等)。如果容器一启动就退出,先用 docker run -it 交互模式运行,确认入口命令是否正确。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

容器完整流程:docker run 背后
https://blog.souloss.com/posts/container-runtime/container-complete-workflow/
作者
Souloss
发布于
2026-05-14
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
容器全景:从 chroot 到 OCI
容器运行时 从 1979 年的 chroot 到 2020 年代的 OCI 标准,容器技术走过了四十年的演进之路。本章建立容器运行时的全景认知——三大内核基石(Namespace/Cgroup/OverlayFS)、OCI 规范体系、Docker/containerd/runc 的架构关系,让你在深入细节前先看到完整的地图。
2
系列导读
容器运行时 本系列从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构,再到容器安全、沙箱运行时、网络、存储、镜像构建、Wasm 容器,最后综合实战构建一个迷你容器运行时——从「会用 Docker」到「理解容器运行时的每一行代码」,每章配有可运行的代码示例与架构图,让你从容器用户进阶到容器运行时工程师。
3
容器网络
容器运行时 容器网络的核心问题是——隔离的 Network Namespace 如何与外部通信?详细解读 veth pair(虚拟网卡对)、bridge(虚拟网桥)、iptables/NAT(地址转换)、CNI(容器网络接口)的完整链路,以及 Docker 的四种网络模式和 Kubernetes 的 Pod 网络模型——从「容器能 ping 通外网」到「理解每一条网络规则」。
4
容器存储
容器运行时 容器的可写层随容器删除而丢失,数据持久化需要 Volume。一网打尽容器存储的完整方案——Volume(绑定挂载/命名卷/tmpfs)、存储驱动(overlay2/devicemapper/btrfs)、CSI(容器存储接口)插件机制,以及 Kubernetes 的 PV/PVC/StorageClass 体系——从「docker run -v」到「理解容器存储的每一条挂载规则」。
5
Docker 容器启动流程:从镜像到运行
原理 深入剖析 Docker 容器从启动命令到运行实例的完整流程——涵盖镜像加载、命名空间隔离、资源限制、联合挂载等核心技术。