创建一个简单的podman容器镜像

发布时间:2026/10/4 8:55:14
创建一个简单的podman容器镜像 在 Ubuntu 26.04 / WSL2 中用 Podman 构建 Hello World 镜像目标以ubuntu:26.04为基础镜像用 Podman 构建一个容器镜像运行时在屏幕上输出hello world。适用环境Ubuntu 26.04 LTSResolute Raccoon2026-04-23 发布原生系统或 Windows 上的 WSL2 Ubuntu 26.04。读者定位你已经会用 Linux 命令行知道sudo、管道、apt是什么但从没接触过容器。本文只解释容器相关的概念与参数通用命令的基础不再赘述。目录容器核心概念先读这一节环境确认安装 Podmanrootless 模式的前置配置拉取基础镜像编写 Containerfile构建镜像运行容器完整脚本常见问题排查1. 容器核心概念先读这一节1.1 镜像与容器镜像Image一个只读的、打包好的文件系统模板内含程序运行所需的一切这里是 Ubuntu 26.04 的文件系统 一条echo命令。它是图纸一旦构建完成就不再改变。容器Container镜像被实例化并运行后的产物。每个容器在镜像的只读层之上额外叠加一个可写层。容器里产生的任何改动都写在这一层不会污染镜像。容器 镜像只读层可被多个容器共享 可写层每个容器独有容器删除即消失这个模型解释了一个新手常见困惑“我在容器里rm了文件、装了软件为什么重建容器又没了”——因为那些改动都在可写层容器一删就没了想永久保留必须改镜像也就是重新构建。1.2 镜像分层Layer镜像不是一个整体而是一层层叠加出来的。Containerfile 里每一条会改变文件系统的指令如RUN、COPY都会产生一个新层。好处复用ubuntu:26.04这一层在网络和磁盘上只存一份一百个基于它的镜像共享这一层。缓存构建时某条指令之前的层只要没变其结果就能直接复用不用重新执行。可追溯podman history 镜像名能看到每一层是哪条指令产生的。理解分层能解释为什么调整指令顺序会影响构建速度把变化频繁的指令放后面能让更多层命中缓存。1.3 容器与虚拟机的区别容器虚拟机隔离级别进程级操作系统内核共享硬件级各自独立内核启动速度毫秒~秒级数十秒级体积通常几 MB~几百 MB通常几 GB隔离强度较弱共享宿主内核较强关键点容器并非轻量虚拟机它本质是被隔离起来的普通进程靠 Linux 内核的namespace隔离视图和cgroup限制资源实现。所以容器里只能跑 LinuxWindows 宿主上要靠 WSL2 提供 Linux 内核本文正是这条路。1.4 rootless 模式传统 Docker 的守护进程以root身份运行任何能访问它的用户实际上就等于拿到了宿主机的 root 权限——这是经典的安全隐患。Podman 的默认形态是 rootless无根它不常驻守护进程直接以当前普通用户的身份运行容器。原理是把容器内的 UID 0root通过user namespace映射到你账号名下预留的一批 UID 上详见 第 4 节。1.5 Podman 与 Docker 的关系语法高度兼容podman build/podman run等命令与 Docker 几乎一一对应Containerfile 与 Dockerfile 语法相同。架构不同Docker 需要守护进程daemonPodman 不需要直接 fork 出容器进程。因此 Podman 可以 rootless、无守护进程运行。迁移成本低多数情况下把命令里的docker换成podman即可。1.6 镜像名的结构一个完整的镜像名由三部分组成[仓库地址/]命名空间/仓库名[:标签] docker.io / library / ubuntu : 26.04仓库地址registry镜像存放的服务器这里是 Docker Hub。命名空间namespacelibrary是 Docker Hub 上官方镜像的固定命名空间。仓库名repository这里即ubuntu。标签tag:26.04是同一仓库下某个具体镜像的别名可理解为书签。省略则默认:latest——但那不代表最新只是维护者打的默认标记内容随时可能变不适合需要可复现的场景。为什么本教程坚持写全docker.io/library/ubuntu:26.04Podman 对只写短名如ubuntu的镜像有短名解析策略可能报错见 Q1。写全地址从源头规避。这是 Podman 与 Docker 一个显眼的行为差异。1.7 Containerfile一个纯文本文件声明怎么从基础镜像造出我的镜像。Podman 逐行读取并执行。Docker 里对应文件叫 Dockerfile二者语法相同。2. 环境确认2.1 Ubuntu 版本lsb_release-a# 关键字段Release: 26.042.2 WSL2仅 Windows 环境wsl--install-d Ubuntu-26.04wsl-l-v# 必须确认 VERSION 为 2为什么必须是 WSL2容器依赖 Linux 内核的 namespace / cgroup而WSL2 提供的是真正的 Linux 内核WSL1 只是系统调用翻译层无法运行容器。若显示 VERSION 为 1执行wsl --set-version Ubuntu-26.04 2转换。关键认知在 WSL2 的 Linux 里Podman 就像在原生 Linux 上一样直接运行不需要 Podman Machine。Podman Machine 是给Windows 原生环境借助 WSL 后端跑 Podman那种场景准备的与本文路线无关。本文所有命令都在 WSL2 的 Ubuntu 终端内执行。3. 安装 Podmansudoaptupdatesudoaptinstall-ypodmanUbuntu 26.04 官方源里的 Podman 版本已经足够新且由发行版维护直接装即可。验证podman--versionpodmaninfo--format{{.Host.OS}} {{.Host.Arch}}关于第二条info输出 Podman 的运行时环境详情存储驱动、内核、可用资源等--format用 Go 模板语法只挑出宿主操作系统和CPU 架构两项。看架构是因为镜像常有 amd64 / arm64 多版本排查拉到的镜像跑不起来时要靠它对齐。4. rootless 模式的前置配置这一节是rootless 容器能跑起来的关键WSL2 与原生 Ubuntu 通用。内容全部围绕容器运行机制建议完整执行。4.1 启用 systemdWSL2 需要sudotee/etc/wsl.conf/dev/nullEOF [boot] systemdtrue [user] default你的用户名 EOF[boot] systemdtrue让 WSL 用 systemd 作 init[user] default指定默认登录用户换成你实际的用户名这样进 WSL 就是普通用户而非 root。警告tee会整份覆盖/etc/wsl.conf。若原文件还有其他配置如[interop]会丢失。改之前先cat /etc/wsl.conf看一眼。改完必须在PowerShell里完整重启 WSLwsl--shutdown这一步不能省/etc/wsl.conf只在 WSL 启动时读取不重启就不生效。重新进入后验证systemctl--version# 能输出版本号即成功4.2 子 UID / 子 GIDrootless 的核心机制问题容器内通常以 root 身份运行程序很多基础镜像的默认用户就是 root。但 rootless 模式下你在宿主机上只是个普通用户不可能真的给出 root 权限。方案Linux 的user namespace允许做 UID 映射。系统预先给你的账号划出一段备用 UID 区间即 subuid / subgid子 UID / 子 GIDPodman 把容器内的 UID 0 映射到这段区间的某个 UID 上。这样容器内程序以为自己是 root能看到 UID 0宿主机上这些进程实际以你预留的那些普通 UID 运行没有真正的 root 权限。检查系统是否已为你配好grep^$(whoami):/etc/subuid /etc/subgid/etc/subuid、/etc/subgid分别存放各用户的子 UID、子 GID 映射范围。有输出形如xxx:100000:65536即从 100000 起给你 65536 个 ID说明已配好跳过本条。无输出则手动添加sudousermod--add-subuids100000-165535 --add-subgids100000-165535$USERpodmansystem migrate--add-subuids/-subgids追加映射区间100000-165535恰好是 65536 个。podman system migrate让 Podman 重新读取映射并重建运行时状态。注意它会停止当前用户所有运行中的容器请在无容器运行时执行。5. 拉取基础镜像podmanpull docker.io/library/ubuntu:26.04podman不加sudorootless 模式见 1.4。pull从远程仓库下载镜像到本地。docker.io/library/ubuntu:26.04完整镜像名结构见 1.6 节。确认podmanimages输出REPOSITORY/TAG/IMAGE ID/CREATED/SIZE五列。应能看到docker.io/library/ubuntu配26.04标签。关于IMAGE ID镜像的唯一标识内容哈希。因为镜像分层相同内容会得到相同 ID。它比名字:标签更可靠——名字标签可以被重新指向别的镜像。6. 编写 Containerfilemkdir-p~/podman-hellocd~/podman-hello构建必须在Containerfile所在目录进行因为构建上下文取自当前目录。创建Containerfile# 基础镜像Ubuntu 26.04 LTS FROM docker.io/library/ubuntu:26.04 # 说明性标签可选 LABEL org.opencontainers.image.titlehello-world \ org.opencontainers.image.descriptionPrints hello world on startup \ org.opencontainers.image.base.nameubuntu:26.04 # 容器启动时输出 hello world CMD [echo, hello world]FROM docker.io/library/ubuntu:26.04声明从哪个镜像开始构建。你的镜像 基础镜像 你叠加的改动这条指令决定了第一层。后续所有指令都在这个基础之上执行。它必须是Containerfile里第一条有效指令注释和ARG除外。LABEL ...给镜像添加元数据键值对属于 OCIOpen Container Initiative容器标准组织约定的规范。它不参与运行纯粹用于描述、检索与自动化标注。org.opencontainers.image.*OCI 标准标签前缀统一命名避免冲突。\续行把一条LABEL折成多行书写可读性更好。因为与输出 hello world这一核心目标无关标注为可选。CMD [echo, hello world]声明容器启动时要执行的默认命令。这是本题的核心也是最容易和RUN混淆的地方单独讲透RUNCMD执行时机构建期podman build时运行期podman run时产生什么一个新的镜像层结果被固化无新层只是记录启动时该跑什么执行次数构建时一次每次启动容器都执行典型用途装软件、编代码、准备环境启动服务、指定默认入口若用RUN echo hello world那行字只会在构建日志里打印一次容器启动时反而什么都不输出。想构建时就顺手验证可以额外加RUN echo hello world构建日志里会看到一次但容器启动时仍以CMD为准。关于[echo, hello world]这种写法这是exec 形式JSON 数组数组首元素是程序名其余是参数。推荐用它因为exec 形式直接执行echo这个可执行文件没有额外 shell 进程行为可预测另一种 shell 形式CMD echo hello world会被包成/bin/sh -c ...执行多一层 shell信号传递与参数解析行为都不同。最终目录结构~/podman-hello/ └── Containerfile文件名叫Dockerfile或ContainerfilePodman 都能识别后者的叫法不绑定特定厂商推荐优先用。7. 构建镜像podmanbuild-thello-world:1.0.build构建子命令。-t hello-world:1.0--tag简写给产出的镜像命名并打标签格式名称:标签。标签由你自定义1.0、v1、日期都行只要自己分得清版本。.构建上下文指定Containerfile所在目录同时界定哪些文件可被构建过程引用。.表示当前目录不能省略——podman build必须知道去哪找Containerfile。构建内部发生了什么对照 1.2 分层理解读取Containerfile检查本地有无基础镜像ubuntu:26.04没有则自动拉取从FROM开始逐条执行每条产生文件系统变化的指令生成一个新层所有层组合成最终镜像打上hello-world:1.0标签。确认构建结果podmanimages|grephello-world# 预期localhost/hello-world 1.0 ...localhost/前缀表示这是本地构建的镜像不来自远程仓库。8. 运行容器podmanrun--rmhello-world:1.0run从镜像创建并启动容器把创建和启动合成一步。--rm容器退出后自动删除容器不是删除镜像。跑完一次就走的命令型容器加上它能保持环境干净想事后查日志就省略它。hello-world:1.0要运行的镜像与构建时打的标签一致。命令行没写echo hello world是因为镜像的CMD已经是它了。命令行里再写的任何内容会覆盖CMD这里无需重复。运行时发生了什么对照 1.1Podman 定位镜像 → 在只读层上叠加一个可写层 → 启动容器执行CMD→echo打印 → 命令结束 → 容器退出 → 因--rm被清理。预期输出hello world只输出这一行就结束是正常的。容器的生命周期与其中运行的进程绑定进程结束容器就退出。它不会像虚拟机那样常驻后台除非你运行的是一个不自结束的进程如 Web 服务器。。9. 完整脚本保存为build.sh#!/usr/bin/env bashset-euopipefailcommand-vpodman/dev/null||{echo未安装 podman请先 sudo apt install -y podman;exit1;}podmanbuild-thello-world:1.0.podmanrun--rmhello-world:1.0与容器构建直接相关的两点command -v podman /dev/null || { ...; exit 1; }运行前先确认 Podman 存在否则给一句人话提示而不是让podman build抛出一串晦涩错误。脚本里的.指的是运行脚本时的当前目录所以必须cd到存放Containerfile的目录后再运行。chmodx build.sh ./build.sh10. 常见问题排查Q1. 短名解析报错现象Error: short-name ... did not resolve to an alias。原因Podman 出于安全考虑对未写全仓库地址的镜像名如ubuntu有短名解析策略默认仓库未配置就会失败。这是 Podman 与 Docker 最显眼的行为差异之一。解决写全镜像名ubuntu:26.04→docker.io/library/ubuntu:26.04本教程已全程写全从源头规避或在/etc/containers/registries.conf中把docker.io设为默认仓库。Q2. WSL2 下systemctl不可用现象System has not been booted with systemd as init system (PID 1)。解决按 4.1 节 配置/etc/wsl.conf并务必wsl --shutdown后重进。注意只运行podman run不需要systemd它只在用到服务管理、编排时才需要。Q3. rootless 报cannot clone: Operation not permitted原因内核禁止非特权用户创建 user namespace或子 UID 映射没配好——即 4.2 节 的前提缺失。解决sudosysctl-wkernel.unprivileged_userns_clone1这是兜底手段WSL2 内核一般默认允许并确认 4.2 节的 subuid/subgid 已正确配置。Q4. 拉取镜像失败原因WSL2 有独立网络栈Windows 上的代理不会自动透传进来。解决在/etc/containers/registries.conf配置镜像加速地址或单独为 WSL2 配置代理或用curl -I https://docker.io先确认连通性。Q5. 容器内中文乱码本教程输出为纯 ASCII不涉及。若以后让容器内程序输出中文出现乱码通常是容器内缺 locale 设置在Containerfile中加ENV LANGC.UTF-8ENV设置镜像级环境变量容器运行期间一直有效。附核心命令速查podmanpull docker.io/library/ubuntu:26.04# 拉取基础镜像到本地podmanimages# 列出本地镜像podmanbuild-thello-world:1.0.# 用当前目录的 Containerfile 构建镜像podmanrun--rmhello-world:1.0# 运行容器输出 hello world退出后自动清理podmanhistoryhello-world:1.0# 查看镜像的每一层对应每条构建指令podmanrmi hello-world:1.0# 删除本地镜像podmanps-a# 查看所有容器含已退出的