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 一张图看懂分层
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 默认使用systemdcgroup 驱动,与 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 规范后,你会发现「用哪个」其实取决于你的部署环境、安全要求和团队习惯。希望这份命令对照表和选型建议,能帮你少踩一些迁移的坑。