Docker vs Podman vs containerd:容器运行时全景

TL;DR 快速导读

  • 运行时分两层:高层级运行时(Docker Engine、containerd、Podman)负责镜像与生命周期编排,低层级运行时(runc)真正启动进程,两者通过 OCI 规范衔接。
  • Docker:生态最完整、上手成本最低,但守护进程架构存在单点故障与 root 提权风险;K8s 1.24 后已不再作为默认运行时。
  • Podman:daemonless + rootless,无守护进程、普通用户即可运行,与 systemd 深度集成,适合服务器与安全敏感场景。
  • containerd:Kubernetes 默认运行时,轻量稳定,适合集群节点与底层可控场景;日常操作推荐用 nerdctl 而非 ctr。
  • 命令迁移 :docker ↔ podman ↔ nerdctl 高频命令基本一一对应,可用 alias docker=podman 平滑过渡,但需注意 --cgroup-manager、网络模式等底层差异。
  • 选型结论:本地开发与团队协作继续用 Docker;追求无守护进程、rootless 与 systemd 集成选 Podman;K8s 集群与边缘场景用 containerd。

1. 引言

容器技术早已不是新鲜词,但很多人在使用容器时,其实并不清楚自己到底在用哪一层运行时。docker run 背后发生了什么?Kubernetes 里跑的又是什么?为什么社区一直在讨论「去 Docker 化」?

这篇文章从容器运行时分层讲起,把 Docker、Podman、containerd 三者的定位、架构、命令差异和选型思路一次讲透,并附上一份 docker ↔ podman ↔ nerdctl 的命令对照表,方便你一键迁移。

2. 运行时分层:Docker / containerd / runc / OCI 规范

要理解 Docker、Podman、containerd 的区别,先要搞清楚容器运行时其实分两层:

  • 高层级运行时(High-level Runtime):负责镜像管理、容器生命周期编排、网络与存储配置,面向用户和编排系统。Docker Engine、containerd、Podman 都属于这一层。
  • 低层级运行时(Low-level Runtime):真正负责启动容器进程、配置 cgroups / namespaces 的组件,典型代表是 runc。

这两层之间通过 OCI(Open Container Initiative)规范 衔接。OCI 定义了镜像格式规范(image-spec)和运行时规范(runtime-spec),只要符合规范,高层级运行时可以自由替换底层 runc,例如换成 gVisor、Kata Containers 等安全容器。

2.1 Docker 的完整调用链

传统 Docker 的调用链大致是:

text 复制代码
docker CLI
  → dockerd(守护进程)
    → containerd(容器生命周期管理)
      → runc(真正启动进程)

也就是说,Docker 本身并不直接启动容器进程,它依赖 containerd 和 runc 完成底层工作。这也是「Docker 里其实已经包含了 containerd」说法的来源。

2.2 一张图看懂分层

flowchart TD A["docker CLI"] --> B["dockerd 守护进程"] B --> C["containerd 高层级运行时"] C --> D["runc 低层级运行时"] D --> E["Linux 内核 cgroups / namespaces"] F["Podman CLI"] --> G["直接调用 runc(无守护进程)"] G --> E H["kubelet"] --> I["containerd(CRI 实现)"] I --> D

3. Docker:生态最完整,但守护进程是双刃剑

Docker 是目前使用最广泛的容器平台,优势在于生态和易用性。

3.1 核心优势

  • 生态成熟:镜像仓库、文档、第三方工具(docker-compose、CI/CD 集成)最丰富。
  • 体验统一 :docker build、docker run、docker compose 一条龙,开发者心智负担小。
  • 调试方便 :docker logs、docker exec、docker stats 等命令开箱即用。

3.2 主要痛点

  • 守护进程架构:所有操作都要经过 dockerd,一旦守护进程崩溃,所有容器管理都会受影响。
  • 权限问题:dockerd 以 root 运行,存在较大的攻击面,历史上出现过多次提权漏洞。
  • 与 Kubernetes 渐行渐远:K8s 1.24 之后移除了对 Docker 作为运行时(dockershim)的支持,转而直接使用 containerd。

4. Podman:daemonless 与 rootless 的践行者

Podman 由 Red Hat 主导开发,设计目标就是「无守护进程、无 root 权限也能跑容器」。

4.1 daemonless 架构

Podman 没有常驻守护进程,每个命令直接通过 fork/exec 方式调用 runc 启动容器。这意味着:

  • 没有单点故障,进程级隔离更彻底。
  • 更安全,普通用户无需 root 即可运行容器。
  • 更贴近 systemd 管理方式,适合在服务器上以 systemd 单元运行容器。

4.2 rootless 模式

Podman 支持 rootless 运行,即普通用户直接运行容器。但 rootless 有一些限制需要注意:

  • 端口映射受限:默认无法绑定 1024 以下端口,且需要 slirp4netns 或 pasta 做用户态网络。
  • 卷挂载权限:rootless 容器内用户与宿主机用户映射关系复杂,挂载宿主机目录时可能出现权限不一致。
  • 存储驱动差异 :rootless 默认使用 fuse-overlayfs,性能略低于 root 模式下的原生 overlayfs。

4.3 podman-compose 兼容

Podman 提供 podman-compose 和 podman play kube 两种方式兼容 Docker Compose 工作流:

bash 复制代码
# 方式一:podman-compose
podman-compose -f docker-compose.yml up -d

# 方式二:将 compose 文件转换为 Kubernetes YAML
podman play kube deployment.yaml

5. containerd:Kubernetes 的默认运行时

containerd 最初是从 Docker 中拆分出来的,现在由 CNCF 托管,是 Kubernetes 默认且最主流的容器运行时。

5.1 与 Kubernetes 的关系

Kubelet 通过 CRI(Container Runtime Interface) 与 containerd 通信。containerd 内置了 CRI 插件,因此 K8s 可以直接调用它,而无需像 Docker 那样经过 dockershim 转换层。

text 复制代码
kubelet
  → CRI 插件(containerd 内置)
    → runc
      → 容器进程

5.2 常用命令:ctr 与 nerdctl

containerd 自带的 CLI 是 ctr,但它的命令设计偏底层、面向调试,不太适合日常使用:

bash 复制代码
# 查看命名空间
ctr namespace ls

# 拉取镜像
ctr images pull docker.io/library/nginx:latest

# 运行容器
ctr run docker.io/library/nginx:latest nginx-demo

更推荐使用 nerdctl,它是 containerd 的 Docker 兼容 CLI,命令风格与 docker 几乎一致:

bash 复制代码
# 拉取并运行
nerdctl run -d -p 8080:80 nginx:latest

# 查看容器
nerdctl ps

# 构建镜像
nerdctl build -t myapp:latest .

5.3 生态短板

containerd 本身不提供镜像构建、网络、日志等高级能力,这些需要依赖外部工具(如 buildkit、nerdctl、CNI 插件)补齐。相比 Docker 全家桶,开箱即用的体验稍弱。

6. 命令对照表:docker ↔ podman ↔ nerdctl 一键迁移

下面是日常高频命令的对照表,方便你从 Docker 平滑迁移:

功能 docker podman nerdctl
拉取镜像 docker pull nginx podman pull nginx nerdctl pull nginx
运行容器 docker run -d nginx podman run -d nginx nerdctl run -d nginx
查看容器 docker ps podman ps nerdctl ps
查看日志 docker logs <id> podman logs <id> nerdctl logs <id>
进入容器 docker exec -it <id> sh podman exec -it <id> sh nerdctl exec -it <id> sh
构建镜像 docker build -t app . podman build -t app . nerdctl build -t app .
查看镜像 docker images podman images nerdctl images
删除容器 docker rm <id> podman rm <id> nerdctl rm <id>
删除镜像 docker rmi <img> podman rmi <img> nerdctl rmi <img>
容器网络 docker network ls podman network ls nerdctl network ls
卷管理 docker volume ls podman volume ls nerdctl volume ls
Compose docker compose up podman-compose up nerdctl compose up

提示:Podman 和 nerdctl 都提供了 alias docker=podman 或 alias docker=nerdctl 的迁移方案,但要注意 --cgroup-manager、网络模式等底层差异,alias 并不能 100% 保证行为一致。

7. 选型建议:什么时候继续用 Docker,什么时候迁移

7.1 继续用 Docker 的场景

  • 本地开发与团队协作:生态最成熟,团队上手成本最低。
  • 依赖 Docker Compose 的复杂编排:Compose 文件生态最完善,第三方工具支持最好。
  • 需要镜像构建与推送一体化:Docker BuildKit 体验成熟,CI 集成方案多。

7.2 建议迁移到 Podman 的场景

  • 追求无守护进程架构:希望消除 dockerd 单点故障。
  • 安全合规要求高:需要 rootless 运行,降低提权风险。
  • 与 systemd 深度集成 :用 podman generate systemd 把容器托管给 systemd。

7.3 建议使用 containerd 的场景

  • Kubernetes 集群节点:K8s 默认且最稳定的运行时,直接使用 containerd 最省心。
  • 追求轻量、底层可控:不需要 Docker 全家桶,只想保留核心容器能力。
  • 边缘计算 / IoT:资源受限环境,containerd 比 Docker 更轻量。

7.4 迁移注意事项

8. 实战:从 Docker 迁移到 Podman 的完整步骤

下面以一台 Ubuntu 22.04 服务器为例,演示从 Docker 平滑迁移到 Podman 的完整流程。假设你已有一个正在运行的 docker-compose.yml 项目。

8.1 安装 Podman 并配置 alias

先安装 Podman:

bash 复制代码
# Ubuntu / Debian
sudo apt update
sudo apt install -y podman podman-compose

# 验证版本
podman --version
podman-compose --version

安装完成后,在 ~/.bashrc 或 ~/.zshrc 中追加 alias,让日常命令无缝切换:

bash 复制代码
# 追加到 ~/.bashrc
echo "alias docker=podman" >> ~/.bashrc
source ~/.bashrc

# 验证 alias 生效
docker --version   # 实际输出 podman 版本

注意:alias 只对交互式 shell 生效。若在脚本或 CI 中调用 docker,建议直接改用 podman 命令,避免依赖 shell 环境。

8.2 迁移 docker-compose.yml 到 podman-compose

podman-compose 兼容大部分 Compose 语法,但有几个字段需要留意:

  • version 字段 :Podman 会忽略顶层 version,可保留也可删除。
  • build 字段 :podman-compose 支持构建,但需要本机已安装 buildah(通常随 podman 一起安装)。
  • ports 映射:rootless 模式下无法绑定 1024 以下端口,需改用高位端口。
  • volumes 权限:挂载宿主机目录时,容器内用户 UID 与宿主机不一致会导致权限问题(见 8.3)。

迁移步骤:

bash 复制代码
# 1. 先停掉 Docker 容器(保留数据卷)
docker compose down

# 2. 用 podman-compose 启动同一份 compose 文件
podman-compose -f docker-compose.yml up -d

# 3. 查看容器状态
podman-compose ps

如果 compose 文件里有 version: "3.8" 之类的顶层字段,建议先去掉:

bash 复制代码
# 去掉顶层 version 字段(可选)
sed -i '/^version:/d' docker-compose.yml

8.3 处理 rootless 模式的端口映射与卷权限

端口映射

rootless 模式下,Podman 默认通过 slirp4netns 做用户态网络,无法绑定 1024 以下端口:

bash 复制代码
# 错误:rootless 下无法绑定 80 端口
podman run -d -p 80:80 nginx:latest
# 报错:Error: cannot listen on the TCP port: listen tcp4 :80: bind: permission denied

# 正确:改用 8080 高位端口
podman run -d -p 8080:80 nginx:latest

如果确实需要绑定 80/443 等低端口,有两种方案:

bash 复制代码
# 方案一:使用 rootful Podman(不推荐,失去 rootless 优势)
sudo podman run -d -p 80:80 nginx:latest

# 方案二:配置内核参数允许非 root 绑定低端口(推荐)
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
# 持久化:echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-unprivileged-ports.conf

卷挂载权限

rootless 容器内默认用户 UID 与宿主机不同,挂载目录时经常遇到 Permission denied:

bash 复制代码
# 查看容器内用户 UID
podman run --rm -v /data:/data alpine id
# 输出:uid=0(root) gid=0(root) 或 uid=1000 等

# 方案一:在 compose 文件中指定 user 字段,与宿主机 UID 对齐
# docker-compose.yml 片段:
# services:
#   app:
#     user: "1000:1000"
#     volumes:
#       - /data:/data

# 方案二:用 --userns=keep-id 保持 UID 映射
podman run -d --userns=keep-id -v /data:/data nginx:latest

# 方案三:直接调整宿主机目录权限(简单粗暴)
sudo chown -R 1000:1000 /data

8.4 用 systemd 管理 Podman 容器

9. 常见问题与排查思路

迁移到 Podman 或使用 containerd 的过程中,难免会遇到一些典型问题。下面整理四个高频场景的诊断命令和解决步骤。

9.1 rootless 模式下容器无法访问外部网络

现象 :容器能启动,但 podman exec 进入容器后 ping 或 curl 外部地址失败,报 Temporary failure in name resolution 或直接超时。

原因 :rootless 模式下 Podman 默认使用 slirp4netns 做用户态网络,DNS 解析由宿主机转发。若 slirp4netns 未正确配置 DNS,或宿主机 /etc/resolv.conf 指向了不可达的 DNS 服务器,容器内就无法解析域名。

诊断命令:

bash 复制代码
# 1. 查看容器内 DNS 配置
podman exec <container> cat /etc/resolv.conf

# 2. 测试 DNS 解析
podman exec <container> nslookup baidu.com

# 3. 测试网络连通性(绕过 DNS)
podman exec <container> curl -v http://223.5.5.5

# 4. 查看 slirp4netns 进程是否正常
ps aux | grep slirp4netns

解决步骤:

bash 复制代码
# 方案一:在容器启动时显式指定 DNS 服务器
podman run -d --dns 223.5.5.5 --dns 8.8.8.8 nginx:latest

# 方案二:修改 ~/.config/containers/containers.conf,全局配置 DNS
# [network]
# dns_servers = ["223.5.5.5", "8.8.8.8"]

# 方案三:改用 pasta 网络后端(新版 Podman 支持,性能更好)
podman run -d --network=pasta nginx:latest

提示:如果宿主机本身能正常上网,但容器内 DNS 失败,优先检查 /etc/resolv.conf 是否被 systemd-resolved 改写。可尝试在容器内手动指定 --dns 验证。

9.2 podman-compose 与 docker-compose 在 depends_on 上的差异

现象 :同一份 docker-compose.yml 在 Docker 下能正常启动,迁移到 podman-compose 后,依赖服务(如数据库)还没就绪,应用服务就抢先启动,导致连接失败。

原因 :Docker Compose 的 depends_on 默认只保证「启动顺序」,而 condition: service_healthy 会额外等待健康检查通过。podman-compose 对 condition 的支持不完整,部分版本会忽略健康检查条件,只按顺序启动。

诊断命令:

bash 复制代码
# 查看 podman-compose 版本
podman-compose --version

# 查看服务启动顺序(观察日志时间戳)
podman-compose -f docker-compose.yml logs -f

解决步骤:

bash 复制代码
# 方案一:在应用服务中增加重试逻辑(推荐,最稳妥)
# 在应用启动脚本中加入等待数据库就绪的循环

# 方案二:改用 podman play kube,用 Kubernetes 的 initContainer 实现依赖等待
podman play kube deployment.yaml

# 方案三:手动控制启动顺序,先启动依赖服务
podman-compose -f docker-compose.yml up -d db
sleep 10
podman-compose -f docker-compose.yml up -d app

提示:如果项目强依赖 depends_on: condition: service_healthy,建议评估迁移到 podman play kube 或 Kubernetes 原生编排,避免在 podman-compose 上反复踩坑。

9.3 容器重启后 IP 变化导致服务不可用

现象 :容器重启后,应用通过容器 IP 访问其他服务失败,报 Connection refused 或超时。

原因:Podman 默认网络模式下,容器每次重启可能获得新的 IP。如果应用配置里硬编码了容器 IP,重启后就会失效。

诊断命令:

bash 复制代码
# 查看容器当前 IP
podman inspect <container> | grep IPAddress

# 查看容器网络
podman network ls
podman network inspect <network>

解决步骤:

bash 复制代码
# 方案一:使用容器名代替 IP(推荐)
# 在应用配置中改用容器名,例如 http://db:3306 而不是 http://172.17.0.2:3306

# 方案二:为容器分配固定 IP
podman network create --subnet 10.89.0.0/24 mynet
podman run -d --network mynet --ip 10.89.0.100 nginx:latest

# 方案三:使用 podman-compose 时,让服务通过服务名互相访问
# 在 docker-compose.yml 中,应用服务直接写 db:3306 即可

提示:在 compose 项目内部,服务之间应始终使用服务名(service name)通信,而不是 IP。Podman 会为同一网络内的服务自动做 DNS 解析。

9.4 containerd 命名空间隔离导致 ctr 看不到 nerdctl 创建的容器

现象 :用 nerdctl run 创建了容器,但用 ctr 查看时却看不到,或者反过来。

原因 :containerd 使用命名空间(namespace)做资源隔离。nerdctl 默认使用 default 命名空间,而 ctr 默认使用 default 命名空间,但两者对命名空间的处理方式不同------nerdctl 默认命名空间是 default,ctr 默认也是 default,但如果你用 nerdctl --namespace=k8s.io 创建了容器,ctr 不带参数就看不到。

诊断命令:

bash 复制代码
# 查看所有命名空间
ctr namespace ls

# 查看 nerdctl 当前使用的命名空间
nerdctl namespace ls

# 在指定命名空间下查看容器
ctr -n k8s.io c ls
nerdctl --namespace=k8s.io ps

解决步骤:

bash 复制代码
# 方案一:让 ctr 和 nerdctl 使用同一个命名空间
ctr -n default c ls
nerdctl --namespace=default ps

# 方案二:统一使用 nerdctl(推荐,命令更友好)
# nerdctl 默认命名空间是 default,日常操作直接用 nerdctl 即可

# 方案三:如果容器在 k8s.io 命名空间(K8s 节点场景)
ctr -n k8s.io c ls
ctr -n k8s.io images ls

提示:在 Kubernetes 节点上,kubelet 通过 CRI 创建的容器都在 k8s.io 命名空间。此时用 ctr 排查必须加 -n k8s.io,否则会误以为「容器丢了」。

Podman 原生支持生成 systemd 单元文件,把容器托管给 systemd,实现开机自启和崩溃自动重启:

bash 复制代码
# 1. 先创建一个容器(以 nginx 为例)
podman run -d --name web --restart=always -p 8080:80 nginx:latest

# 2. 生成 systemd 单元文件
mkdir -p ~/.config/systemd/user
podman generate systemd --name web --files --new

# 3. 重新加载并启用服务
systemctl --user daemon-reload
systemctl --user enable --now container-web.service

# 4. 查看服务状态
systemctl --user status container-web.service

如果希望 systemd 在系统启动时自动拉起用户服务,需要启用 linger:

bash 复制代码
# 允许用户服务在无登录会话时运行
sudo loginctl enable-linger $USER

# 验证
loginctl show-user $USER | grep Linger

对于 compose 项目,也可以生成对应的 systemd 单元:

bash 复制代码
# 基于 compose 项目生成 systemd 服务
podman-compose -f docker-compose.yml up -d
podman generate systemd --name myproject --files --new

迁移完成后,建议用 podman ps、podman logs 验证服务状态,并观察一段时间确认无异常后再彻底卸载 Docker。

  • --cgroup-manager 差异 :Podman 默认使用 systemd cgroup 驱动,与 Docker 的 cgroupfs 不同,在 K8s 或 systemd 环境下需保持一致。
  • 网络模式差异:Podman 的 rootless 网络基于 slirp4netns,端口映射行为与 Docker 有差异。
  • 存储驱动差异:rootless Podman 使用 fuse-overlayfs,性能与原生 overlayfs 有差距。
  • 镜像构建能力:containerd 本身不提供 build,需要额外安装 buildkit 或使用 nerdctl。

8. 总结

  • Docker:生态最完整,守护进程架构带来便利也带来风险,适合本地开发与团队协作。
  • Podman:daemonless + rootless,安全性和 systemd 集成是亮点,适合服务器与安全敏感场景。
  • containerd:Kubernetes 默认运行时,轻量稳定,适合集群节点与底层可控场景。

三者并非互斥,而是面向不同场景的合理选择。理解运行时分层和 OCI 规范后,你会发现「用哪个」其实取决于你的部署环境、安全要求和团队习惯。希望这份命令对照表和选型建议,能帮你少踩一些迁移的坑。

相关推荐
vipxieliang1 小时前
数据持久化方案:卷、挂载、权限与备份恢复
docker·容器
vipxieliang1 小时前
Compose 生产级配置:从 demo 到可上线
docker
vipxieliang1 小时前
容器原理揭秘:namespace、cgroup 与镜像分层
docker·容器
wzq11_6663 小时前
Kubernetes集群——基础篇(基础知识与搭建步骤一遍过!!!)
java·容器·kubernetes
java_logo4 小时前
Docker 部署 dockurr/windows:轻松搭建浏览器可控 Windows 虚拟机
运维·windows·docker·容器·虚拟机·kvm·轩辕镜像
谢亮_vipxieliang5 小时前
私有镜像仓库:Registry、Harbor 与镜像同步实战
网络·docker·容器
努力努力再努力wz7 小时前
【边缘计算入门系列】从“在哪里算”到“怎么算”:一文建立边缘计算、算子、计算图与 Tensor 的底层心智模型
人工智能·docker·边缘计算
江湖有缘7 小时前
3款开源IT工具箱整理合集,可Docker一键部署!
docker·容器·开源
旋生万物7 小时前
用螺旋数重写 Transformer Attention:让大模型自带“相位记忆“的 PyTorch 实现
docker·云原生·kubernetes·螺旋生成论·螺旋相位