容器镜像不像酒, 越陈越香;而是更像奶酪, 越陈越臭.

Podman是Pod Manager的缩写。Podman解决了Docker一直存在的某些问题,其中最重要的两点是:

  1. Podman提供了增强的安全性
  2. 非root权限执行命令的能力

Pod是Kubernetes项目中普遍存在的一个概念,一个pod可以有一个或者多个容器,这些容器共享相同的命名空间和控制组。Podman项目组将Podman描述为“用于在Linux系统上开发、管理和运行OCI容器的无守护进程的容器引擎。容器既可以以特权模式运行,可以以非特权模式运行”。Podman的目标是可以在Linux上自然的运行容器,从而充分利用Linux平台的所有功能。

  1. 02 山西·晋中·榆次

术语

容器

容器是在Linux系统上运行的一组进程.容器确保一个进程组不会干扰系统上的其他进程, 恶意进程不能支配系统资源.容器通过以下方式进行隔离:

  1. 资源限制( cgroups )
    1. 一组进程可以使用的内存量
    2. 进程可使用的CPU核数
    3. 进程可使用的网络资源数量
  2. 安全限制
    1. 放弃Linux能力来限制root用户的权限
    2. 通过SELinux控制对文件系统的访问
    3. 只读访问内核文件系统
    4. 通过seccomp限制内核中可用的系统调用
    5. 通过用户命名空间将主机上的一组UID映射到另一组UID, 从而允许访问有限特权环境
  3. 命名空间
    1. 网络命名空间
    2. 挂载命名空间
    3. PID命名空间

镜像

容器镜像通过将所有软件捆绑到一个单元中的方式, 解决了依赖管理问题, 可以将所有软件库\可执行文件和配置文件一并交付.

  1. 容器镜像格式

    1. 包含应用程序所需的所有软件的目录树
    2. 描述rootfs内容的JSON文件
    3. 被称为manifest列表的JSON文件, 用于将多个镜像连接在一起以支持不同的硬件架构
  2. 容器运行方式

    1. 镜像使用Linux tar工具将rootfs和JSON文件打包在一起, 然后上传到容器镜像注册服务器
    2. 容器引擎会将JSON文件\引擎内置默认值和用户输入合并来创建新的容器OCI运行时贵方的JSON文件
    3. 容器运行时读取容器的JSON文件\内核cgroups\安全约束和命名空间, 并最终启动容器的主进程
  3. 容器标准

    1. OCI标准委员会
    2. OCI镜像格式
    3. OCI运行时规范

Podman

  1. 支持fork/exec模型,容器时命令的子进程
  2. 无守护进程,软件升级时不停止容器
  3. 支持kubernetes类pod和基于kubernetetes yaml启动容器
  4. 支持非特权容器
  5. 支持REST API
  6. 与systemd集成
  7. 自定义容器镜像注册服务器
  8. 支持多种传输方式
  9. 完全可定制

特权容器

# 关闭docker的安全限制, 并挂载本地根目录
( base ) [root@docker ~]# docker run -it --name hacker --privileged -v /:/host redhat/ubi8 chroot /host
Unable to find image 'redhat/ubi8:latest' locally
latest: Pulling from redhat/ubi8
c9235833a899: Pull complete
Digest: sha256:2e863fb65e595839633113300a0c6709e9af81523a30001290068265c121a927
Status: Downloaded newer image for redhat/ubi8:latest
sh-4. 2# ls /
bin boot data dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
sh-4. 2# cd /root
sh-4. 2# ls
Anaconda3-2023. 09-0-Linux-x86_64. sh

如果普通用户拥有docker的权限, 就可以获得”为所欲为”的权限, 删除容器就可以抹除所有操作记录.

基础命令

参见:Podman使用入门

Pod

Podman pod和Kubernetes pod一样,包含一个infra容器,或者被称为pause容器。该容器负责维持内核的命名空间和cgroups的状态,允许容器在pod内的创建与销毁。infra容器由一个名为conmon的容器监控进程来监控,而且pod中的每个容器都有属于自己的conmon。

  1. conmon负责执行OCI运行时,将OCI规范文件的路径和容器层挂载点
  2. conmon负责监控容器的生命周期
  3. conmon负责处理用户到容器的连接
  4. conmon负责生成pod的日志文件
# 创建一个空的pod,也就是一个网络命名空间,并将容器内部端口8080绑定到主机上的8080端口
$ mkdir html
$ podman pod create -p 8080:8080 --name mypod --volume ./html:/var/www/html:z
a086f23c67ae68ba79399d8dcb09217440e43f9079608607059bf8f929b97845
# 列出当前的pod
$ podman pod ps
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
a086f23c67ae mypod Created About a minute ago be5b8119c711 1
# 可见实际只有pause容器,并无业务容器,端口也已经映射
$ podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
be5b8119c711 localhost/podman-pause:5. 3. 1-1732147200 reated 0. 0. 0. 0:8080->8080/tcp a086f23c67ae-infra
# 添加pod, Web程序的实际应用
$ podman create --pod mypod --name myapp ubi8/httpd-24
# 创建应用脚本
$ cat > ./html/time.sh <<EOF
#!/bin/bash
data() {
echo "<html><head></head><h1>"; date; echo "Hello World</h1></body></html>"
sleep 1
}
while true ; do
data > index.html
done
EOF
$ chmod +x ./html/time.sh
# 添加边车容器, 将/var/www/html目录作为默认目录并运行打印时间戳脚本
$ podman create --pod mypod --name time --workdir /var/www/html ubi8 ./time.sh
# 启动pod
$ podman pod start mypod
# 列出pod
$ podman pod ps
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
c8a185241126 mypod Running 4 minutes ago b2e753fc5766 3
# 查看pod属性
$ podman pod inspect mypod
[
{
"Id": "c8a185241126ef5370ca62f32a2b87286dbc6fe29a6a1de6203f2cb790ffbb16",
"Name": "mypod",
"Created": "2025-02-05T21:18:04. 411942531+08:00",
"CreateCommand": [
"podman",
"pod",
"create",
"-p",
"8080:8080",
"--name",
"mypod",
"--volume",
"/root/html:/var/www/html:z"
],
"ExitPolicy": "continue",
"State": "Running",
"Hostname": "",
"CreateCgroup": true,
……
# 检查
$ curl 192. 168. 24. 10:8080
<html><head></head><h1>
Wed Feb 5 13:31:26 UTC 2025
Hello World</h1></body></html>
# 停止pod
$ podman pod stop mypod
# 删除pod
$ podman pod rm mypod

设计

自定义和配置文件

存储位置

# 配置文件位置
# root权限下系统podman配置存储文件位置
root@docker:~# podman info --format '{{ . Store. ConfigFile }}'
/usr/share/containers/storage.conf
# 普通用户配置存储文件位置
$ podman info --format '{{ . Store. ConfigFile }}'
/home/sujx/. config/containers/storage.conf

# 默认配置位置
root@docker:~# vim /usr/share/containers/storage.conf
[storage]
driver = "overlay"
runroot = "/run/containers/storage"
graphroot = "/var/lib/containers/storage"

通过修改graproot参数可以修改镜像和容器存储位置。Podman使用container/storage支持多种不同类型的分层文件,被称为写时复制(CoW)文件系统,Podman默认使用overlay驱动程序。

容器镜像注册服务器

# 配置文件位置
root@docker:~# cat /etc/containers/registries.conf
# 镜像注册服务器数组,按顺序依次拉取镜像
unqualified-search-registries = ["registry.fedoraproject.org", "registry.access.redhat.com", "docker.io", "quay.io"]
# 配置docker hub代理,每个镜像注册服务器独立配置
[[registry]]
prefix="docker.io"
location="hub. 1panel.dev"
# 允许未加密http连接
insecure=true
# 禁止镜像下载
blocked=true
# 允许使用短命称
short-name-mode = "enforcing"

引擎配置

# 引擎配置文件位置
root@podman:~# vim /usr/share/containers/containers.conf

container.conf支持以下配置项:

  1. [containers] 运行单个容器的配置
  2. [engine] Podman的默认配置
  3. [service_destinations] 远程服务配置
  4. [secrets] 有关secrets插件驱动程序信息
  5. [network] 网络特殊配置

系统配置

当你在非特权模式下运行Podman时,将使用 /etc/subuid和/etc/subgid 文件来制定容器的UID范围。Podman会读取这俩个文件以获取分配给你的用户账户UID和GID范围。然后Podma启动 /usr/bin/newuidmap 和 /usr/bin/newgidmap ,验证podman制定的UID和GID范围是否实际分配给你。某些情况下,需要手动修改这些文件以便添加UID。

与Systemd集成

Systemd是Linux事实上的init系统。Podman接受systemd的强大功能,Podman鼓励用户将systemd单元文件与Podman命令一起使用。

在容器中运行systemd

在容器中将systemd作为初始PID运行,然后允许systemd启动容器内的一个或多个服务。这种思路认为,容器化服务应该像在虚拟机中一样启动。这种方式允许在同一容器中运行多个服务,利用本地通信路径,加快将大型多服务应用程序转换为容器。另外,systemd在容器中的另一个巨大优势是init系统会处理僵尸程序的清理工作,这样当容器进程异常退出的适合,如果PID1不回收它们,它们就会悬挂并且永不消失。

Podman的设计旨在支持两种方法——微服务和多服务容器。Podman检查容器的cmd选项,然后为init系统启动systemd,接着它自动以systemd模式启动容器。

[sujx@docker ~]$ podman pull ubi9-init
[sujx@docker ~]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
registry.access.redhat.com/ubi9-init latest 2e10efedbde9 3 days ago 251 MB
# 容器内程序启动是由init启动
[sujx@docker ~]$ podman inspect ubi9-init --format '{{ . Config. Cmd }}'
[/sbin/init]
# 常规容器的启动
# 以nginx为例
[sujx@docker ~]$ podman inspect nginx --format '{{ . Config. Cmd }}'
[nginx -g daemon off;]
# 以ubuntu为例
[sujx@docker ~]$ podman inspect ubuntu --format '{{ . Config. Cmd }}'
[/bin/bash]

# 如果无参数启动systemd容器,可以看到容器启动后是先启动systemd
# 此时通过Ctrl-C是无法停止容器,只有通过其他终端来终止
[sujx@docker ~]$ podman run -ti ubi9-init
systemd 252-46. el9_5. 2 running in system mode ( +PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN -IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK +XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified )
Detected virtualization container-other.
Detected architecture x86-64.

Welcome to Red Hat Enterprise Linux 9. 5 ( Plow )!

Initializing machine ID from container UUID.
Queued start job for default target Graphical Interface.
[ OK ] Created slice Slice /system/modprobe.
[ OK ] Started Dispatch Password Requests to Console Directory Watch.
[ OK ] Started Forward Password Requests to Wall Directory Watch.
[ OK ] Reached target Local File Systems.
[ OK ] Reached target Network is Online.
[ OK ] Reached target Path Units.
[ OK ] Reached target Remote File Systems.
[ OK ] Reached target Slice Units.
[ OK ] Reached target Swaps.

# podman通过systemd发生停止信号,杀死最近启动的容器
[sujx@docker ~]$ podman stop -l

容器化的systemd要求

  1. 在/run上挂载tmpfs
  2. 在/tmp上挂载tmpfs
  3. 将/var/log/journald作为tmpfs
  4. container环境变量
  5. STOPSIGNAL=SIGRTMIN+3 systemd接受这个信号才会彻底退出

systemd模式下的Podman容器

# 创建systemd容器,使用--systemd=always标志以systemd模式启动容器
[sujx@docker ~]$ podman create --rm --name SystemD -ti --systemd=always ubi9-init sh
592a5aaa4eeaedc901a38de957ff709d97e50ec8083e222ebeefbb11dba40120
# 停止参数
[sujx@docker ~]$ podman inspect SystemD --format '{{ . Config. StopSignal }}'
SIGRTMIN+3
# 查看tmpfs挂载
[sujx@docker ~]$ podman start --attach SystemD
sh-5. 1# mount |grep -e /tmp -e /run | head -2
tmpfs on /run/secrets type tmpfs ( rw, nosuid, nodev, relatime, size=394904k, nr_inodes=98726, mode=700, uid=1000, gid=1000, inode64 )
tmpfs on /run/. containerenv type tmpfs ( rw, nosuid, nodev, relatime, size=394904k, nr_inodes=98726, mode=700, uid=1000, gid=1000, inode64 )
sh-5. 1# printenv container
oci

# 创建一个httpd服务的容器Dockerfie
cat >> /home/sujx/podman/Containerfile<EOF
FROM ubi9-init
RUN dnf -y install httpd; dnf -y clean all
RUN systemctl enable --now httpd.service
EOF
# 生成容器
[sujx@docker ~]$ podman build -t my-systemd /home/sujx/podman
# 拉起服务
[sujx@docker ~]$ podman run -d --rm -p 8080:80 -v ./html/:/var/www/html:Z my-systemd
8746e0bf313a4d3c84535623393567f148d4c3d762ce5683b53c679dc9510308
[sujx@docker ~]$ curl localhost:8080
<h1>Goodbye Workd!</h1>

使用journald进行日志记录

# podman默认使用journald来记录日志
[sujx@docker ~]$ podman info --format '{{ . Host. LogDriver }}'
journald
# 输出事件
[sujx@docker ~]$ podman run --rm --name test2 ubi9 echo "Check if logs persist"
Check if logs persist
# 使用主机journalctl来查看容器日志
[sujx@docker ~]$ journalctl -b |grep "Check if logs persist"
Feb 20 21:54:37 docker test2[3249]: Check if logs persist

在系统启动时启动容器

重启容器

Podman通过在systemd单位文件中配置Podman的启动模式。如果容器崩溃或者系统重新启动,就可能需要配置相关参数。

选项 功能 启动时重启
no 容器退出时不重启容器 ×
on-failure 当容器以非零状态退出时重启 ×
always 容器退出时重启,无论其退出状态如何,重试次数无限
# 配置重启策略
[sujx@docker ~]$ podman run -d --restart=always --name Ubuntu ubuntu
cb7a068d99b56107db3afe0b1955d5f210964ae97cecd01e1e85205346b92810

使用Podman作为系统服务

common进程也在systemd服务中运行,用以监视容器进程。systemd不知道容器的存在,它只指导运行在单元文件中的进程,包括容器进程。

Podman具有生成包含最佳默认值的单位文件的功能。

# 创建容器
[sujx@docker ~]$ podman create -p 8080:80 --name my-Web nginx
24c372bff6a8cec5ce8f11eca604dd3d84987b17e6585f1d840bf10783991881
# 创建user权限的启动文件
[sujx@docker ~]$ mkdir -p $HOME/. config/systemd/user
[sujx@docker ~]$ podman generate systemd my-Web > $HOME/. config/systemd/user/my-Web.service
# 查看service单元文件
[sujx@docker ~]$ cat $HOME/. config/systemd/user/my-Web.service
[Unit]
Description=Podman container-684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125. service
Documentation=man:podman-generate-systemd( 1 )
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/run/user/1000/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman start 684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125
ExecStop=/usr/bin/podman stop \
-t 10 684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125
ExecStopPost=/usr/bin/podman stop \
-t 10 684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125
PIDFile=/run/user/1000/containers/overlay-containers/684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125/userdata/conmon.pid
Type=forking

[Install]
WantedBy=default.target

# 启动服务
[sujx@docker ~]$ systemctl --user daemon-reload
[sujx@docker ~]$ systemctl --user start my-Web.service
[sujx@docker ~]$ systemctl --user status my-Web.service
my-Web.service - Podman container-684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125. service
Loaded: loaded ( /home/sujx/. config/systemd/user/my-Web.service; disabled; preset: disabled )
Drop-In: /usr/lib/systemd/user/service.d
└─10-timeout-abort.conf
Active: active ( running ) since Fri 2025-02-21 11:25:38 CST; 8s ago
Docs: man:podman-generate-systemd( 1 )
Process: 1460 ExecStart=/usr/bin/podman start 684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6a>
Main PID: 1484 ( conmon )
Tasks: 2 ( limit: 4580 )
Memory: 17. 6M ( peak: 32. 9M )
CPU: 84ms
CGroup: /user.slice/user-1000. slice/user@1000. service/app.slice/my-Web.service
# 校验服务
[sujx@docker ~]$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
# 关闭服务
[sujx@docker ~]$ systemctl --user stop my-Web

分发systemctl单元文件

我们可以通过–new这个开关重新生成一个systemd单元文件,用于在其他系统中使用。这样服务在重启的时候候,会拉取镜像和创建新的容器。

# 重新生成单元文件
[sujx@docker ~]$ podman generate systemd --new my-Web > $HOME/. config/systemd/user/my-Web.service
# 单元文件中容器的EexcStart命令修改为podman run,而不是原来的podman start
[sujx@docker user]$ cat my-Web.service
[Unit]
Description=Podman container-684673ead27728b8946376cab1ebfc2472cf9f550bcefd1161934ce7d6acc125. service
Documentation=man:podman-generate-systemd( 1 )
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--sdnotify=conmon \
-d \
--replace \
-p 8080:80 \
--name my-Web nginx
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

自动更新容器

Podman可以管理自己的更新,每个节点会监视容器镜像注册服务器中是否出现新的镜像。当新的镜像出现时,节点会拉取镜像并重新创建容器。

这个功能的开启条件有两个:

  1. 容器必须在使用podman generate systemd –new 生成的systemd单元文件中运行
  2. 容器具有特殊的标签 –label “io.containers.autoupdate=registry/local”
# 创建带标签容器
[sujx@docker ~]$ podman create --label "io.container.autoupdate=regitstry" -p 8081:80 --name my-Http httpd
# 创建单元文件
[sujx@docker ~]$ podman generate systemd my-Http --new >> $HOME/. config/systemd/user/my-Http.service
# 启动服务
[sujx@docker ~]$ systemctl --user daemon-reload
[sujx@docker ~]$ systemctl --user enable --now my-Http.service
# 手动升级, 自动拉取所有带标签的容器的新镜像并重启服务
[sujx@docker ~]$ podman autoupdate
# 定时更新,每天检查一次
[sujx@docker ~]$ systemctl --user enable --now podman-auto-update.service

Podman服务

Podman项目支持RESTful API,可以使用podman system service命令创建监听服务应用于响应Podman的API调用。

# 启动podman的restful API套接字服务
[sujx@docker ~]$ systemctl --user enable podman.socket
[sujx@docker ~]$ systemctl --user start podman.socket
[sujx@docker ~]$ ls $XDG_RUNTIME_DIR/podman/podman.sock
/run/user/1000/podman/podman.sock
# 使用curl探测podman的版本信息
[sujx@docker ~]$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://d/v1. 0. 0/libpod/version |jq
{
"Platform": {
"Name": "linux/amd64/fedora-40"
},
"Components": [
{
"Name": "Podman Engine",
"Version": "5. 3. 1",
"Details": {
"APIVersion": "5. 3. 1",
"Arch": "amd64",
"BuildTime": "2024-11-21T08:00:00+08:00",
"Experimental": "false",
"GitCommit": "",
"GoVersion": "go1. 22. 7",
"KernelVersion": "6. 12. 15-100. fc40. x86_64",
"MinAPIVersion": "4. 0. 0",
"Os": "linux"
}
},
{
"Name": "Conmon",
"Version": "conmon version 2. 1. 12, commit: ",
"Details": {
"Package": "conmon-2. 1. 12-2. fc40. x86_64"
}
},
{
"Name": "OCI Runtime ( crun )",
"Version": "crun version 1. 19. 1\ncommit: 3e32a70c93f5aa5fea69b50256cca7fd4aa23c80\nrundir: /run/user/1000/crun\nspec: 1. 0. 0\n+SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +LIBKRUN +WASM:wasmedge +YAJL",
"Details": {
"Package": "crun-1. 19. 1-1. fc40. x86_64"
}
}
],
"Version": "5. 3. 1",
"ApiVersion": "1. 41",
"MinAPIVersion": "1. 24",
"GitCommit": "",
"GoVersion": "go1. 22. 7",
"Os": "linux",
"Arch": "amd64",
"KernelVersion": "6. 12. 15-100. fc40. x86_64",
"BuildTime": "2024-11-21T08:00:00+08:00"
}
# 使用curl连接podman的默认输出
# 查询本地镜像列表
[sujx@docker ~]$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://d/v1. 0. 0/libpod/images/json |jq
[
{
"Id": "2e10efedbde980518ddeae0d6d42d6d503b1915e2b09edfe2d9ed93453ae4018",
"ParentId": "",
"RepoTags": [
"registry.access.redhat.com/ubi9-init:latest"
],
"RepoDigests": [
"registry.access.redhat.com/ubi9-init@sha256:c80641132b5ba47ce8d806f48405f78085c508f4e6db7d619d208d9752cf53b1",
"registry.access.redhat.com/ubi9-init@sha256:ea7611fcd37de2e2d1b79eac0255efa89eb1f7e82bfecf91739d56f9674f2fe2"
],
"Created": 1739777526,
"Size": 251470246,
"SharedSize": 0,
"VirtualSize": 251470246,

使用Python与Podman交互

我们可以是以哦那个docker-py与Podman服务进行交互

# 安装工具
[sujx@docker ~]$ dnf -y install podman-py
# 编辑python脚本,podman-py默认链接Podman套接字
[sujx@docker ~]$ cat >./image.py<<EOF
import podman
client=podman. PodmanClient()
print( client.images.list( all=True ))
EOF
# 执行测试
[sujx@docker ~]$ python3 image.py
[<Image: 'registry.access.redhat.com/ubi9-init:latest'>, <Image: 'docker.io/library/nginx:latest'>, <Image: 'docker.io/library/ubuntu:latest'>, <Image: 'docker.io/library/httpd:latest'>, <Image: 'docker.io/library/busybox:latest'>]

参考

  1. 了解容器内部和外部的 root
  2. 无根容器的背后发生了什么
  3. 弄懂什么是REST API