Tao
Tao

精通 podman run 使用指南

现代容器生态系统的核心在于将静态的容器镜像转变为动态的、正在运行的进程。对于 Podman 容器引擎而言,这一关键功能由 podman run 命令封装。作为一个无守护进程、开源且为 Linux 原生的工具,Podman 旨在利用开放容器倡议 (OCI) 的容器和镜像来查找、运行、构建和部署应用程序。

podman run 命令是运行容器的核心工具,可以说是所有容器操作的基础。如果你之前用过 Docker,会发现 podman run 的使用方式很相似,但它的底层设计更加先进。Podman 最大的特点是更安全、与系统集成更好。比如说,它不需要一个后台守护进程一直运行(这样就不会有单点故障的风险),而且支持普通用户直接运行容器,不需要管理员权限,这大大提高了安全性。

本文基于 podman run 命令提供较为详细的指南。它超越了对命令标志和选项的简单罗列,旨在对该命令的功能、其底层架构及其在高级、真实世界工作流中的作用进行全面分析。目标用户是为系统管理员、DevOps 工程师和开发人员提供必要的细致理解,以便利用 podman run 的全部功能,实现安全、高效和可扩展的容器部署。本文将涵盖从日常基本操作到使用 SELinux 和 AppArmor 进行高级安全加固、实际应用部署,以及将容器作为一流服务集成到现代 Linux 系统中的所有内容。

在深入研究高级配置和安全加固之前,必须对 podman run 命令的核心目的、语法和基本机制的理解。该命令是 Podman 套件中功能最丰富的命令,因为它负责协调新容器的整个设置和启动过程,让操作员能够最终控制其运行时环境。

podman run 命令的主要目的是在指定的镜像之上创建一个新的、可写的容器层,并在该容器内执行一个命令。此操作会实例化一个拥有自己隔离环境的进程,包括专用的文件系统、独特的网络堆栈和独立的进程树。尽管源镜像可能定义了默认行为,例如要执行的命令或要暴露的网络端口,但 podman run 赋予了操作员在运行时覆盖这些默认设置的能力,从而对容器的配置进行精细控制。

Podman 及其 podman run 命令在 OCI 兼容的生态系统内运行。这意味着它依赖于一个标准的容器运行时,如 runc 或 crun,来与 Linux 内核交互并创建正在运行的容器进程。这种对开放标准的遵守确保了由 Podman 创建的容器与由 Docker 或 CRI-O 等其他容器引擎创建的容器几乎没有区别,从而促进了互操作性并防止了供应商锁定。

该命令的结构设计灵活,通过一致的语法适应各种配置。其最基本的结构如下:

bash

podman run [选项] 镜像 [命令 [参数...]]

每个部分都有其独特的目的:

  • podman run:启动容器创建和执行过程的基础命令。
  • [选项]:一系列可选标志,用于修改容器的配置和运行方式。这些标志控制网络、存储、安全、资源限制和执行模式等方面。由于该命令的综合性作用,它支持的选项比任何其他 Podman 命令都多。
  • 镜像:用作新容器蓝图的容器镜像名称。这可以是本地镜像,也可以是来自远程仓库的镜像。镜像可以使用 transport:path 格式指定,其中 docker://(用于远程仓库)是默认的传输方式。
  • [命令 [参数…]]:一个可选的命令及其相应的参数,在容器内部执行。如果提供,这将覆盖镜像的 Containerfile 或 Dockerfile 中指定的默认 CMD 指令。

podman run 命令封装了一系列构成容器生命周期初始阶段的动作。这个自动化的工作流通过在单个操作中处理多个步骤来简化部署。

首先,Podman 检查指定的镜像是否存在于本地存储中。如果未找到,podman run 将自动联系系统中 registries.conf 文件配置的容器仓库,以查找并拉取该镜像及其所有依赖项。这种行为确保了必要的组件可用,而无需单独执行 podman pull 命令。

一旦镜像在本地可用,Podman 就会基于它创建一个新容器。在此创建过程中,它会设置容器的隔离环境。这包括在容器内部自动生成几个关键文件,如 /etc/hosts/etc/hostname/etc/resolv.conf,这些文件用于管理网络,并且通常基于主机的配置。此外,还会在 /run/.containerenv 创建一个文件,为容器内的进程提供一种标准方式来检测它们是否在容器化环境中运行。

环境配置完成后,podman run 在新容器内执行指定的命令。整个工作流最简单的演示是使用 hello-world 镜像:

bash

$ podman run hello-world

Hello from Podman!
This message shows that your installation appears to be working correctly.

在此示例中,Podman 执行以下步骤:

  1. 在本地检查 hello-world 镜像。如果未找到,则从公共仓库(例如 Docker Hub)拉取该镜像。
  2. 从该镜像创建一个新容器。
  3. 在容器内运行可执行文件,该文件将消息打印到控制台。
  4. 容器随后退出,由于它是一个简单的、短暂的任务,它会停止。

这个序列表明,podman run 命令不仅仅是一个执行触发器,而是一种微型编排。它在单个原子操作中管理依赖解析(镜像拉取)、环境配置(网络文件、环境变量)和进程启动。这种视角有助于解释该命令为何拥有如此广泛的选项集;它不仅仅是"运行"一个进程,而是在精心构建该进程将要生存的隔离世界。这种全面的控制能力正是 podman run 成为所有基于容器的工作负载的基础工具的原因。

虽然 podman run 提供了大量的选项,但其中一个核心子集构成了日常容器管理的基础。掌握这些基本标志对于执行基本操作至关重要,例如运行交互式会话、管理后台服务、确保数据持久性以及配置容器的网络和环境。

容器的运行模式是操作员做出的最基本的选择之一。

对于需要直接用户交互的任务,如访问 shell 或调试应用程序,交互模式是必不可少的。这通过组合两个标志来实现:-i(或 --interactive),它保持容器的标准输入 (STDIN) 打开;以及 -t(或 --tty),它分配一个伪终端。这种组合允许用户的终端直接连接到容器的进程。

一个常见的用例是在 Ubuntu 容器内启动一个 bash shell:

bash

$ podman run -it ubuntu bash
root@f8d05968b4a2:/# 

对于长时间运行的服务,如 Web 服务器、数据库或 API,需要在后台运行容器。-d(或 --detach)标志实现了这一点,指示 Podman 启动容器然后分离控制台,从而释放用户的终端以执行其他命令。在分离模式下运行时,podman run 会打印出新容器的唯一 ID。可以使用 podman ps 命令查看这些后台容器的状态。

当容器正在运行时,可以使用 podman attach 附加到其标准流。对于以 -it 启动的交互式容器,用户可以使用 ctrl-p,ctrl-q 组合键从会话中分离,而不会停止容器。这个组合键可以通过 --detach-keys 选项进行配置。

正确管理容器身份和生命周期是维护一个干净、有组织的主机系统的关键。

默认情况下,Podman 会为每个新容器分配一个随机生成的名称(例如 laughing_bob)。虽然功能正常,但这些名称不易记忆。--name 标志允许用户分配一个人类可读的名称,这简化了后续的管理命令,如 podman stoppodman logspodman rm

bash

podman run -d --name web_server nginx

许多容器是为短暂的、临时的任务创建的,例如运行测试套件或一次性脚本。为了防止这些容器在退出后占用主机的存储空间,应使用 --rm 标志。此选项会在容器的主进程终止后立即自动移除容器的文件系统,确保不留下任何残留物。这是维护系统卫生的关键最佳实践。

默认情况下,容器创建时带有自己隔离的网络堆栈,这意味着无法从主机或外部网络访问它们。端口映射是用于暴露容器化应用程序的机制。

-p(或 --publish)标志将主机上的一个端口映射到容器内的一个端口,格式为 -p hostPort:containerPort。这告诉 Podman 将到达指定 hostPort 的任何网络流量转发到容器的 containerPort。例如,要运行一个 Nginx Web 服务器并使其在主机的 8080 端口上可访问:

bash

podman run -d --name web -p 8080:80 nginx

现在,可以通过在 Web 浏览器中访问 http://localhost:8080 或使用 curl 等工具来访问容器内的应用程序。

-P(或 --publish-all)标志提供了一个方便的快捷方式,它会将容器镜像中所有暴露的端口发布到主机上随机的高位端口。这对于动态分配端口而不用担心冲突非常有用。

容器文件系统本质上是短暂的;写入容器内的任何数据在容器被移除时都会丢失。为了使数据在单个容器的生命周期之外持久化,必须使用卷或绑定挂载。

绑定挂载直接将主机文件系统中的文件或目录映射到容器的文件系统中。这通过 -v(或 --volume)标志实现,语法为 -v /path/on/host:/path/in/container。绑定挂载非常适合向容器提供源代码、配置文件或其他主机端资产。

虽然绑定挂载与主机上的特定路径相关联,但命名卷是由 Podman 直接管理的存储实体。它们是持久化应用程序数据(如数据库文件)的首选方法,因为它们将数据与主机的文件系统结构解耦。要使用命名卷,只需提供一个名称而不是主机路径:

bash

podman run -d --name my_database -v db_data:/var/lib/mysql/data mysql

如果名为 db_data 的卷不存在,Podman 将创建它。此卷随后可以被其他容器重用,从而方便升级和数据迁移。

-v--mount 标志可以附加选项来控制挂载的行为。最常见的是 :ro,表示只读访问,这可以防止容器修改挂载的内容。其他关键选项包括 SELinux 重标记标志 :z:Z,这对于允许容器访问启用 SELinux 的系统上的主机文件至关重要,并将在第 7 节中详细探讨。

可以使用几个关键选项自定义容器内应用程序的运行时环境。

-e(或 --env)标志在容器内设置一个环境变量。这通常用于传递配置参数,例如数据库凭据或应用程序模式。例如,配置一个 MySQL 容器:

bash

podman run -d --name db -e MYSQL_ROOT_PASSWORD=secretpassword -e MYSQL_DATABASE=myapp mysql:5.7

对于管理大量变量,可以使用 --env-file 标志从一个简单的行分隔文本文件中加载它们。

理解镜像的 ENTRYPOINT 和 CMD 之间的区别很重要。ENTRYPOINT 是容器启动时运行的主要可执行文件,而 CMD 为该可执行文件提供默认参数。

podman run --entrypoint 标志允许操作员覆盖镜像的默认 ENTRYPOINT。在 podman run 命令中镜像名称之后提供的任何参数都将覆盖镜像的默认 CMD。

-w(或 --workdir)标志设置容器内命令将要执行的工作目录。这对于期望从文件系统中特定位置运行的应用程序非常有用。

为了巩固这些基础知识,下表可作为这些基本标志的快速参考指南:

标志 用途 示例
-d, --detach 在后台运行容器(分离模式) podman run -d nginx
-it 创建一个带有伪 TTY 的交互式会话 podman run -it ubuntu bash
--name 为容器分配一个自定义名称 podman run --name my-db postgres
--rm 容器退出时自动移除 podman run --rm hello-world
-p, --publish 将主机端口映射到容器端口 (主机:容器) podman run -p 8080:80 httpd
-v, --volume 将主机路径或命名卷挂载到容器中 podman run -v ./config:/etc/app:ro my-app
-e, --env 在容器内设置环境变量 podman run -e APP_MODE=production my-app

要理解 podman run 命令,不仅仅是记住它的选项;还需要深入理解 Podman 与其他容器引擎(尤其是 Docker)区别开来的架构哲学。Podman 的无守护进程设计不仅仅是一个技术实现细节——它是其在安全性、审计和系统集成方面主要优势的基础原则。

Podman 和 Docker 之间最显著的架构差异在于是否存在一个中央守护进程。

Docker 采用客户端-服务器架构。当用户执行像 docker run 这样的命令时,docker CLI 客户端本身并不运行容器。相反,它通过 Unix 套接字向一个名为 Docker 守护进程(dockerd)的长期运行的后台进程发送 REST API 请求。这个守护进程通常以 root 权限运行,负责管理整个容器生命周期:拉取镜像、创建和启动容器、管理网络以及处理存储。该守护进程是一个单一的、庞大的控制点,也是一个潜在的单点故障。

Podman 从根本上拒绝了这种模型。它在没有持久的、特权守护进程的情况下运行。当用户执行 podman run 时,该命令直接与内核的 API 和 OCI 运行时(runc)交互来创建容器。容器进程成为启动它的 podman 命令的直接子进程,遵循传统的 Unix 进程的 fork/exec 模型。这消除了中间的守护进程,简化了架构,并在用户、命令和容器之间建立了更直接的关系。

这种架构上的分歧对容器的管理和安全方式产生了深远的影响。

没有一个由 root 拥有的守护进程是 Podman 最大的安全优势。在 Docker 模型中,守护进程的 root 权限代表了一个巨大的攻击面;如果攻击者能够攻破守护进程,他们实际上就获得了对主机系统的 root 控制权。因为 Podman 没有这样的守护进程,所以这类漏洞被完全消除了。在无根 Podman 环境中,容器逃逸被限制在执行 podman run 命令的用户的有限权限内,从而极大地减少了系统范围的损害潜力。

无守护进程模型提供了更清晰的审计跟踪。在使用审计守护进程(auditd)的 Linux 系统上,由 Docker 容器执行的操作被记录为源自 dockerd 进程,而不是启动容器的用户。这使得将潜在的恶意活动追溯到特定的用户账户变得极其困难。对于 Podman,由于容器是用户命令的直接子进程,auditd 会正确地将所有操作归因于调用 podman run 的用户,从而确保了问责制并简化了取证分析。

通过放弃专有的守护进程进行生命周期管理,Podman 可以直接与标准的、健壮的 Linux 系统组件集成。其中最重要的是 systemd,现代 Linux 发行版的通用服务管理器。Podman 不依赖内部机制来处理容器重启和后台服务,而是可以生成 systemd 服务单元。这使得容器可以像任何其他原生系统服务一样进行管理,使用熟悉的命令如 systemctl startsystemctl enablesystemctl status。这种方法将容器视为操作系统的头等公民,而不是由一个独立的、庞大的应用程序管理的对象。

虽然不总是主要考虑因素,但 Podman 的直接管理模型可以带来更快的容器启动时间。通过消除客户端和守护进程之间的 API 调用开销,有时可以更快地实例化容器。

关于无守护进程模型的一个常见问题是,分离的容器(-d)在初始的 podman run 命令退出后如何持续存在。答案在于一个名为 conmon(容器监视器)的小型、轻量级实用程序。

当执行 podman run 时,它并不直接启动 OCI 运行时(runc)。相反,它为容器启动一个专用的 conmon 进程。这个 conmon 进程随后调用 runc 来创建容器,并随后充当容器的父进程。

conmon 的职责包括:

  • 监视容器的主进程并捕获其退出代码。
  • 通过中继其标准输出和错误流来处理容器的日志记录。
  • 管理交互式会话的伪终端(TTY),使 podman attach 能够工作。
  • 通过向容器进程发送适当的信号来响应像 podman kill 这样的命令。

通过将这些监视任务委托给每个容器的最小 conmon 进程,主要的 podman CLI 可以退出,从而实现分离操作,而无需一个单一、沉重、集中的守护进程。这种方法清晰地反映了 Unix 哲学:使用小型、专门的工具,做好一件事,与 Docker 的庞大守护进程形成对比,后者为所有容器处理所有这些任务。这种模块化是 Podman 设计的关键推动力,将范式从单一、包罗万象的工具转变为一套与主机操作系统原生集成的可互操作组件(如 Podman、Buildah 和 Skopeo)。

下表总结了由其核心架构差异驱动的 podman rundocker run 工作流之间的关键区别:

特性 podman run docker run
架构 无守护进程。podman CLI 使用 conmon 和 OCI 运行时直接 fork 容器进程。 客户端-服务器。docker CLI 通过 REST API 与中央 dockerd 守护进程通信。
默认用户 默认无根。以当前非特权用户的权限运行容器。 默认有根。dockerd 守护进程及其容器进程以 root 用户身份运行。
安全模型 攻击面更小。容器逃逸仅限于主机用户的权限。审计跟踪清晰地识别用户。 特权守护进程是一个巨大的攻击面和单点故障。审计跟踪被混淆,指向守护进程而非用户。
服务管理 与 systemd 原生集成,将后台容器作为系统服务进行管理。 守护进程内部处理所有服务管理(例如,重启策略)。
Pod 具有原生的 Pod 概念,用于对共享资源的容器进行分组,模仿 Kubernetes 模型。 缺乏原生的 Pod 概念;需要外部工具如 Docker Compose 或编排器如 Swarm。

Podman 最受赞誉和最具影响力的安全特性是其对无根容器的一流支持。此功能允许一个标准的、非特权的用户创建、运行和管理一个完整的容器生态系统,而无需 sudo 或主机系统上的任何提升权限。这不仅仅是一种便利;它代表了容器安全态势的根本性转变,从一个限制特权的模式转变为从一开始就在没有特权的情况下操作的模式。

无根容器是指容器引擎(Podman)和容器进程都在没有 root 权限的情况下执行的容器。这种设计的主要动机是缓解容器逃逸漏洞。在传统的、有根的容器环境中,如果一个恶意进程成功逃脱容器的限制,它会立即在主机系统上获得 root 访问权限,导致整个系统被攻破。而使用无根容器,这种逃逸的"爆炸半径"被大大减小。一个逃逸的进程将只拥有启动容器的非特权用户的有限权限,从而阻止其修改关键系统文件、访问其他用户的数据或在系统范围内安装恶意软件。

实现无根容器的核心技术是 Linux 用户命名空间。用户命名空间在主机上的一系列用户 ID(UID)和组 ID(GID)与命名空间内的一组独立的 UID 和 GID 之间创建了一个映射。这个映射在两个系统文件中配置:/etc/subuid/etc/subgid。这些文件为每个用户指定一个起始的从属 ID 和他们被允许在自己创建的命名空间中使用的可用 ID 数量。

例如,/etc/subuid 中的一个条目 testuser:100000:65536 授予用户 testuser 一个包含 65536 个 UID 的池,从主机上的 UID 100000 开始。

当 testuser 运行一个无根容器时,Podman 使用这个映射来创建用户命名空间:

  • 用户在主机上的 UID(例如,testuser 的 UID 1001)被映射到容器命名空间内的 UID 0 (root)。
  • 容器内的 UID 1 被映射到主机上的 UID 100000。
  • 容器内的 UID 2 被映射到主机上的 UID 100001,依此类推。

这种巧妙的重映射意味着,在容器内以 root 身份运行的进程实际上在主机上是以非特权的 testuser 身份运行的。这可以通过检查主机上的进程列表来验证。这种机制在容器内为需要它的软件提供了 root 权限的假象,而从未在主机系统上授予实际的 root 权限。

newuidmapnewgidmap 实用程序,通常由 uidmap 或 shadow-utils 包提供,是 Podman 执行此映射操作所必需的。

无根模型的优势是显著的,但它也引入了一些用户必须了解的实际限制。

  • 显著增强的安全性:如前所述,这是主要的好处,可以防止权限提升攻击。
  • 多用户隔离:无根容器允许多个非特权用户在同一台机器上同时运行容器,而不会相互干扰。每个用户的容器和镜像都存储在他们自己的主目录中(通常是 $HOME/.local/share/containers/storage),提供了自然的隔离。这在高性能计算(HPC)和共享开发环境中尤其有价值。
  • 开发者灵活性:它使那些没有系统 root 访问权限的开发者能够在环境中使用容器来构建和测试应用程序。
  • 特权端口:根据内核设计,非特权进程不能绑定到低于 1024 的网络端口。这意味着无根容器默认情况下不能在标准端口如 80 (HTTP) 或 443 (HTTPS) 上暴露服务。常见的解决方法是修改一个系统范围的内核参数:sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80。然而,必须理解的是,这一更改允许系统上所有非特权应用程序(不仅仅是 Podman)绑定到这些端口,这可能会有更广泛的安全影响。

  • 主机文件系统访问:无根容器受到启动它的用户的标准 Linux 权限的约束。它不能读取或写入用户自己无法访问的主机上的任何目录。

  • 网络性能:历史上,无根网络依赖于 slirp4netns,这是一个用户空间网络实现,与有根容器使用的内核级网络相比,会引入性能开销。现代 Podman 安装默认使用 pasta,它显著提高了性能,但了解这种区别对于理解旧设置或解决网络问题很重要。

  • 资源控制 (cgroups):在使用旧版 cgroups v1 的系统上,委托资源控制的能力有限,这意味着像 --memory--cpus 这样的资源约束可能无法对无根容器完全强制执行。对无根资源管理的完全支持需要一个运行 cgroups v2 的系统。

本教程演示了无根容器执行的原则和限制。

首先,确保当前用户在 /etc/subuid/etc/subgid 中有条目。如果没有,必须由系统管理员添加。

bash

grep $(whoami) /etc/subuid /etc/subgid

作为非 root 用户,运行一个 Nginx 容器,将其内部端口 80 映射到主机上的一个高端口(>= 1024)。这应该无需任何特殊配置即可成功。

bash

$ podman run -d --name rootless_web -p 8080:80 docker.io/library/nginx
# 验证它正在运行
$ podman ps
# 测试连接性
$ curl http://localhost:8080 

这表明基本的无根操作是直接的。

现在,尝试运行同一个容器,但映射到标准的 HTTP 端口 80。这将会因为权限错误而失败。

bash

$ podman run -d --name failed_web -p 80:80 docker.io/library/nginx
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf

这个错误直接说明了特权端口的限制。

为了允许绑定到端口 80,一个特权用户必须修改系统的 sysctl 配置。

bash

sudo sysctl net.ipv4.ip_unprivileged_port_start=80

应用了解决方法后,非 root 用户现在可以成功地在端口 80 上运行容器。

bash

$ podman run -d --name successful_web -p 80:80 docker.io/library/nginx
# 验证它正在运行并且可以在端口 80 上访问
$ curl http://localhost:80

这个实践练习强调了,虽然无根操作是安全的首选和默认模式,但与特权系统资源的交互仍然需要刻意的、通常是系统范围的配置更改。

podman run 命令远不止是执行容器的简单指令;它是一个复杂、安全且与系统集成的容器化方法的核心。通过超越其表面的命令语法,拥抱其底层架构和集成工具,操作员可以以一种为现代数据中心和云原生领域设定新标准的安全、稳定和系统内聚水平来构建、部署和管理容器化应用程序。

Podman 的无守护进程和无根优先的架构、与强制访问控制框架的深度集成,以及与 systemd 和 Kubernetes 的原生集成,使其成为现代容器管理的强大工具。掌握 podman run 命令及其丰富的选项集,为安全、高效和可扩展的容器部署奠定了坚实的基础。

想要深入了解 Podman 的更多高级特性和配置选项,建议查阅以下官方资源:

这些资源将为你提供最新的功能介绍、详细的配置说明以及社区支持,帮助你在生产环境中充分发挥 Podman 的强大功能。

相关内容