用 Podman 替代 Docker:从迁移到跑通 docker-compose
Posted on 一 27 4月 2026 in Tech
| Abstract | 用 Podman 替代 Docker,并支持 docker-compose |
|---|---|
| Authors | Walter Fan |
| Category | Tech |
| Status | v1.0 |
| Updated | 2026-04-27 |
| License | CC-BY-NC-ND 4.0 |
短大纲
- Docker Desktop 从 2022 年开始对商业用户收费,不少公司开始审计 License。Podman 是 Red Hat 主导的开源替代方案,无守护进程,无 root 要求,CLI 几乎 100% 兼容 Docker。
- 迁移不难,关键是三件事:装好 Podman + 初始化虚拟机 + 设好 docker 别名。
- docker-compose 在 Podman 上有两条路:用
podman compose(内置,调 docker-compose)或用podman-compose(Python 独立实现)。 - 最后用一个 Python Flask + MySQL 的 compose 例子跑通全流程。
正文
为什么要换?
2022 年 Docker 改了 License:Docker Desktop 对 250 人以上或年收入超 1000 万美元的公司不再免费。消息一出,好多公司的法务开始找开发团队"聊聊"。
其实对个人用户和小公司没影响,Docker Desktop 依然免费。但如果你在大公司写代码,或者 CI 环境里不想依赖一个需要 root 权限的 daemon,那 Podman 值得看一眼。
Podman 是 Red Hat 主导的容器引擎,全称 Pod Manager。它和 Docker 的核心区别就三条:
| 特性 | Docker | Podman |
|---|---|---|
| 架构 | Client-Server,需要 dockerd 守护进程 | 无守护进程(daemonless),直接 fork/exec |
| 运行权限 | 默认需要 root(可配 rootless) | 默认 rootless |
| Pod 支持 | 无原生 Pod 概念 | 原生支持 Pod(和 K8s Pod 对齐) |
| CLI 兼容 | — | 几乎 100% 兼容 docker 命令 |
| License | Apache 2.0(引擎),Desktop 商业收费 | Apache 2.0,全免费 |
一句话:Podman 干的活和 Docker 一样,但不需要一个后台 daemon 跑着,也不需要 root。
macOS 上安装 Podman
在 Linux 上 Podman 直接跑容器。但在 macOS 上,容器本来就跑在 Linux VM 里——Docker Desktop 藏了一个 LinuxKit VM,Podman 也一样,只是它用的是 QEMU 或 Apple Virtualization Framework。
# 用 Homebrew 安装
brew install podman
# 初始化虚拟机(第一次需要)
podman machine init
# 启动虚拟机
podman machine start
# 验证
podman info
podman run --rm hello-world
跑完 hello-world 能看到输出,说明环境没问题。
如果你想让所有 docker 命令自动走 Podman,加个别名:
# 加到 ~/.zshrc 或 ~/.bashrc
alias docker=podman
这样你原来的脚本和习惯都不用改。
让 docker-compose 跑起来
这是大家最关心的问题。Podman 自己不带 compose,但有两条路:
路线一:podman compose(推荐)
从 Podman 4.7 开始,podman compose 作为内置子命令存在。它实际上是调用你系统里装好的 docker-compose(Go 版本的 Compose V2)。
# 先装 docker-compose(只是 compose 工具,不需要 Docker Desktop)
brew install docker-compose
# 然后直接用
podman compose up -d
podman compose ps
podman compose down
原理很简单:Podman 启动一个兼容 Docker API 的 socket,compose 工具连这个 socket 来管理容器。需要设一下环境变量:
# Podman 的 Docker 兼容 socket
export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
不过从 Podman 5.x 开始,podman compose 会自动处理 socket,多数情况不用手动设。
路线二:podman-compose(Python 实现)
podman-compose 是一个独立的 Python 项目,用 Podman CLI 来实现 compose 的功能,不依赖 Docker socket。
pip install podman-compose
# 用法和 docker-compose 一样
podman-compose up -d
podman-compose ps
podman-compose down
两条路选哪条?我的建议:
- 如果你的
docker-compose.yml比较复杂(volumes、networks、depends_on、healthcheck 都用了),走路线一,兼容性更好。 - 如果你不想装任何 Docker 相关的东西,走路线二,纯 Podman 生态。
实战:Python Flask + MySQL
光说不练假把式。下面用一个最小但完整的例子:一个 Flask Web App 连 MySQL,用 docker-compose.yml 编排,全程用 Podman 跑。
项目结构
podman-flask-demo/
├── docker-compose.yml
├── app/
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
└── db/
└── init.sql
1. Flask 应用
app/requirements.txt:
flask==3.1.*
pymysql==1.1.*
cryptography==44.*
app/app.py:
import os
import time
import pymysql
from flask import Flask, jsonify
app = Flask(__name__)
DB_CONFIG = {
"host": os.environ.get("DB_HOST", "db"),
"port": int(os.environ.get("DB_PORT", 3306)),
"user": os.environ.get("DB_USER", "demo"),
"password": os.environ.get("DB_PASSWORD", "demo123"),
"database": os.environ.get("DB_NAME", "demo_db"),
}
def get_db():
"""获取数据库连接,带简单重试"""
for attempt in range(10):
try:
return pymysql.connect(**DB_CONFIG, cursorclass=pymysql.cursors.DictCursor)
except pymysql.OperationalError:
if attempt < 9:
time.sleep(2)
else:
raise
@app.route("/")
def index():
return jsonify({"status": "ok", "message": "Flask + MySQL on Podman"})
@app.route("/users")
def list_users():
conn = get_db()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT id, name, email FROM users")
rows = cursor.fetchall()
return jsonify(rows)
finally:
conn.close()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
app/Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
2. 数据库初始化
db/init.sql:
CREATE DATABASE IF NOT EXISTS demo_db;
USE demo_db;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES
('Walter Fan', 'walter@example.com'),
('Alice Chen', 'alice@example.com'),
('Bob Zhang', 'bob@example.com');
3. docker-compose.yml
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: demo_db
MYSQL_USER: demo
MYSQL_PASSWORD: demo123
ports:
- "3306:3306"
volumes:
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
web:
build: ./app
ports:
- "5000:5000"
environment:
DB_HOST: db
DB_PORT: 3306
DB_USER: demo
DB_PASSWORD: demo123
DB_NAME: demo_db
depends_on:
db:
condition: service_healthy
volumes:
mysql_data:
4. 用 Podman 跑起来
cd podman-flask-demo
# 启动(自动 build + 拉镜像 + 启动容器)
podman compose up -d
# 查看状态
podman compose ps
# 等 MySQL 健康检查通过后,测试接口
curl http://localhost:5000/
# {"message":"Flask + MySQL on Podman","status":"ok"}
curl http://localhost:5000/users
# [{"email":"walter@example.com","id":1,"name":"Walter Fan"}, ...]
# 查看日志
podman compose logs web
podman compose logs db
# 停止并清理
podman compose down
# 如果想连数据卷一起删
podman compose down -v
整个过程和 Docker 完全一样。你的同事如果用 Docker Desktop,同一个 docker-compose.yml 也能直接跑,互不影响。
迁移踩坑清单
实际迁移不是装完就万事大吉。几个容易踩的坑:
| 坑 | 表现 | 解法 |
|---|---|---|
| VM 未启动 | Cannot connect to Podman |
podman machine start |
| 镜像拉不下来 | 国内网络超时 | 配镜像加速:编辑 ~/.config/containers/registries.conf |
| rootless 端口限制 | 绑定 1024 以下端口失败 | 用 podman machine set --rootful 或映射到高端口 |
| compose 找不到 socket | Cannot connect to Docker daemon |
设 DOCKER_HOST 环境变量 |
| volume 权限问题 | 容器内读写挂载目录报 Permission denied | 加 :Z 后缀(SELinux)或检查 uid 映射 |
| 某些 Docker 特有功能 | docker buildx、docker scout 不可用 |
这些是 Docker 独有扩展,Podman 有自己的替代(podman build 支持多阶段) |
镜像加速配置
在国内网络环境下,拉 Docker Hub 镜像经常超时。Podman 的镜像源配置方式和 Docker 不一样:
# macOS 上需要进入 Podman VM 来配
podman machine ssh
# 编辑(或创建)配置文件
sudo vi /etc/containers/registries.conf
添加镜像加速:
unqualified-search-registries = ["docker.io"]
[[registry]]
prefix = "docker.io"
location = "docker.io"
[[registry.mirror]]
location = "mirror.gcr.io"
保存后退出 VM,重启 Podman machine 生效。
Podman 独有的好处
说完兼容性,聊几个 Podman 自己的加分项:
1. 原生 Pod 支持
Podman 可以把多个容器编成一个 Pod,共享网络命名空间——和 Kubernetes Pod 的语义一致。
# 创建一个 Pod
podman pod create --name my-pod -p 5000:5000
# 在 Pod 里跑容器
podman run -d --pod my-pod --name web my-flask-app
podman run -d --pod my-pod --name db mysql:8.0
同一个 Pod 里的容器用 localhost 互访,不需要 Docker network。
2. 生成 Kubernetes YAML
这个功能对要从 compose 迁移到 K8s 的团队很实用:
podman generate kube my-pod > my-pod.yaml
直接生成可以丢给 kubectl apply 的 YAML。
3. Systemd 集成
在 Linux 服务器上,Podman 可以生成 systemd unit 文件,让容器跟着系统启动:
podman generate systemd --name my-container --new > my-container.service
不需要 daemon,不需要 Docker 那套 restart policy,直接走 systemd。
什么时候不该换?
公平地说,Podman 不是万能替代:
- Docker Desktop 的 GUI 和开发者体验:如果你依赖 Docker Desktop 的 Kubernetes 集成、Extension Marketplace、Volume Management UI,Podman Desktop 虽然也有 GUI,但功能还差一截。
- Docker BuildKit 的高级特性:cache mount、secret mount 等 BuildKit 特性,Podman 支持了大部分,但偶尔有边界 case 不一致。
- 团队统一性:如果团队其他人都在用 Docker,你一个人换 Podman 可能增加沟通成本。
我的判断:个人开发和 CI 环境,Podman 完全够用,甚至更好(rootless 天然更安全)。生产部署大多走 K8s,不直接依赖 Docker 还是 Podman。
总结
迁移到 Podman 的核心步骤就四步:
brew install podman && podman machine init && podman machine startalias docker=podman(可选,让旧脚本不用改)brew install docker-compose(让podman compose有后端可调)- 原来的
docker-compose.yml直接用,不需要改一行
行动清单:
- [ ] 在开发机上装好 Podman,跑通
hello-world - [ ] 把现有项目的
docker-compose.yml用podman compose up跑一遍,记录不兼容的地方 - [ ] CI 环境切换:把
docker命令替换为podman,观察一周 - [ ] 如果在国内,提前配好镜像加速,别到拉镜像时才发现超时