用 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 buildxdocker 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 的核心步骤就四步:

  1. brew install podman && podman machine init && podman machine start
  2. alias docker=podman(可选,让旧脚本不用改)
  3. brew install docker-compose(让 podman compose 有后端可调)
  4. 原来的 docker-compose.yml 直接用,不需要改一行

行动清单:

  • [ ] 在开发机上装好 Podman,跑通 hello-world
  • [ ] 把现有项目的 docker-compose.ymlpodman compose up 跑一遍,记录不兼容的地方
  • [ ] CI 环境切换:把 docker 命令替换为 podman,观察一周
  • [ ] 如果在国内,提前配好镜像加速,别到拉镜像时才发现超时

参考