容器运行时是真正负责把镜像变成运行中容器的底层组件,没有它,镜像只不过是一堆静态文件,镜像定义"想让它跑成什么样",运行时决定"实际怎么跑起来"并守住隔离边界。
容器运行时到底是什么
从一条 docker run 命令说起
你在终端敲下 docker run 时,Docker 会先解析参数、从镜像仓库拉取镜像,然后调起后端的运行时组件,行业共识认为,完整链条由 Docker 客户端、dockerd 守护进程、containerd 和 runc 四层协作完成,前两层管"人的意图",后两层才是干活的运行时本体。
- containerd 负责管理镜像传输、存储和容器生命周期,可以理解为"大管家"
- runc 是真正的执行者,它调用 Linux 内核的 namespace 和 cgroup 特性,把容器进程"关进"独立的运行空间
近年来越来越多的生产集群直接使用 containerd 而非 Docker 作为 kubelet 的运行时,原因就在于跳过 dockerd 这一层,链路更短、开销更小。
runc 是运行时的"心脏"
无论上层是 Docker、containerd 还是 Podman,最终创建容器进程的几乎都是 runc,它做的事情很纯粹:根据一份 OCI 标准配置,在独立命名空间里启动一个进程,同时施加 CPU、内存、磁盘 IO 等资源限制,你可以把它想象成一个极其严格的"包工头"图纸(镜像里的配置)到位,它就按图施工,不多做一分也不少做一分。
容器运行时和镜像之间如何协作
整个过程可以拆成四个清晰的步骤,每一步都有对应命令可供验证。
第一步:镜像被"展开"成 rootfs
镜像由多层只读文件系统叠加组成,每一层对应 Dockerfile 里的一条指令,运行时拿到镜像后,并不直接使用镜像文件,而是通过 snapshotter 组件把各层读取出来,合并挂载成一个完整的 rootfs,你可以在宿主机上执行 docker inspect <容器ID> 查看 GraphDriver 字段,jq 输出里会显示每层的挂载路径,这就是运行时的"施工现场"。
第二步:创建可写层

容器启动后,对文件系统的所有写入都发生在镜像顶层新创建的可写层中,这意味着你在容器里删除或修改任何文件,都不会触碰到底层镜像,容器被删除后,可写层随之销毁,镜像本身毫发无损这是容器"轻量"和"可复用"两个核心特性的底层来源。
第三步:拼接运行配置并启动进程
运行时读取镜像元数据中的 Entrypoint 和 Cmd,再叠加用户通过 docker run 传入的参数,生成最终的进程启动命令,以 nginx 镜像为例,Entrypoint 是 /docker-entrypoint.sh,Cmd 是 nginx -g "daemon off;",容器运行时将它们拼接成一条完整命令,在隔离环境中启动 nginx 主进程。
第四步:通过标准接口让上层系统"看到"容器
启动完成后,containerd 会通过 CRI 插件向 kubelet 汇报容器状态,Docker 通过 socket 文件将信息返回给 docker CLI,你执行 docker ps 看到的每一行输出,本质上都是运行时向上层汇报的"工作日志"。
容器运行时和虚拟机有什么区别
很多人容易混淆"运行时"和"虚拟化技术",二者的核心差异在于隔离边界。
隔离粒度不同
虚拟机通过 Hypervisor 模拟完整硬件,每个 VM 里跑一个独立操作系统内核,隔离粒度是"整台机器",容器则直接复用宿主机内核,通过 namespace 做进程隔离,隔离粒度是"单个进程组"。
开销模型不同
- 虚拟机启动需要经过 BIOS、内核引导、init 系统初始化,通常耗时数十秒
- 容器启动只 fork 一个进程并挂载 rootfs,耗时通常百毫秒级
- 虚拟机镜像动辄几个 GB,容器镜像一般只有几十到几百 MB
安全边界认知
业内的共性认知是:虚拟机适合强隔离场景,比如多租户云平台;容器适合高频发布和弹性伸缩场景,如果你在做多租户业务且安全要求极高,选虚拟机;如果你追求交付速度和资源密度,容器运行时更合适。
docker运行时和containerd怎么选

这个问题在 2026 年前后成为社区讨论的焦点,因为 Kubernetes 在 1.24 版本彻底移除了 dockershim。
选 containerd 的典型场景
- 你运行的是 K8s 集群,节点需要轻量化,内存占用尽量低
- 你希望减少故障排查链路,只有 kubelet → containerd → runc 三层
- 你不需要 docker build 镜像、docker compose 编排的图形化体验
选 Docker 的典型场景
- 本地开发调试,需要镜像构建、日志查看、端口映射等完整的开发工具链
- 单机部署多个应用,用 docker-compose 管理 dependencies
- 使用 Docker Desktop 在 Windows 或 macOS 上做开发,mac 上跑 Linux 容器依赖轻量虚拟机
推荐策略:开发环境用 Docker,生产集群用 containerd,二者其实不冲突,Docker 本身也内置了 containerd,只是被包裹在了更上层。
从实操角度理解运行时的位置
查看当前节点使用的运行时
# Docker 环境
docker version --format '{{.Server.Runtime}}'
# Kubernetes 节点
kubectl get nodes -o wide
kubectl describe node <node-name> | grep "Container Runtime"
手动调起一个容器(绕过 Docker)
先安装 crictl,然后直接向 containerd 发请求:
# 拉取镜像到 containerd crictl pull docker.io/library/nginx:latest # 创建并启动容器 crictl run container.json pod.json
执行 crictl ps -a 能看到容器状态,再用 crictl logs <容器ID> 查看输出,这一步能直观感受到运行时和镜像的关系镜像只是静态包,运行时负责把它变成有生命周期的 Pod 容器。
镜像存在哪里
Docker 镜像默认存储在 /var/lib/docker/overlay2/ 下,containerd 镜像存储在 /var/lib/containerd/ 下,overlay2 目录里每一个子目录对应一个镜像层,目录里包含 diff 和 link 文件,diff 存真实文件内容,link 记录指向父层的引用,容器启动时,containerd 按顺序叠加这些目录形成完整的视图,这就是你在容器里看到的所有文件。

一个容易忽略的细节:镜像体积与运行时启动速度的关系
镜像层越多,运行时需要挂载的层就越多,启动速度会下降,有实际测试表明,基础镜像从 200MB 缩减到 50MB,容器冷启动时间可缩短 30% 以上,因此生产环境建议使用 Alpine 或 Distroless 这类精简镜像,进入容器后安装不必要的包会让可写层膨胀,增加 delete 操作的时间消耗。
用户提问环节
容器运行时和虚拟机技术栈选哪个?
如果是个人开发环境,Docker Desktop 内置的轻量虚拟机最省心;如果是生产 K8s 集群,直接选 containerd 作为运行时,多租户场景叠加 gVisor 或 Kata Containers 做额外隔离,把 Docker 当作开发工具,把 containerd 当作生产组件,这已经是多数技术团队的共识。
镜像下载慢和运行时有什么关系?
镜像拉取由 containerd 或 dockerd 内部的 distribution 组件负责,与底层 runc 无关,拉取慢通常是因为镜像体积大或网络带宽受限,可以通过配置 mirror 加速源、使用多阶段构建减小镜像体积、开启 containerd 的懒加载镜像功能来改善,懒加载允许只在容器启动前加载必要的数据块,而非一次性拉取全部镜像层,远程读镜像的比例较高时效果显著,推迟到容器运行时与实际访问数据时再按需拉取,最终结果是启动耗时接近实时加载。
为什么修改了 Dockerfile 但运行时启动后没有生效?
先确认是否构建了新镜像,docker build 时会利用层缓存,Dockerfile 中 COPY 的文件内容没变,这一层会复用旧缓存,导致镜像内容未更新,执行 docker build --no-cache 强制重建,再 docker run 验证修改是否生效,如果仍不生效,则检查启动时是否挂载了宿主机目录覆盖了容器内的默认路径比如手动挂载了 volume 到 /etc/nginx,此时镜像里的配置文件中文件不会覆盖宿主机挂载上来的配置,这是运行时读不到预期镜像文件中最常见的原因。