RHEL 中級 #7 コンテナ入門 — Podman/Buildah/Skopeo
この記事で RHEL 中級シリーズを締めくくります。最後のテーマはコンテナ、より正確には RHEL 9 が標準として採択した Podman です。Docker とコマンドは似ていますが内部構造はかなり異なります。デーモンがなく、一般ユーザー権限でコンテナを立ち上げ、systemd とも自然に統合されます。この違いを理解せずに入ると Docker 式の習慣をそのまま持ってきてミスしやすいです。この記事ではその違いを運用観点から整理します。
RHEL 中級 シリーズでこの記事の位置:
- #1 SELinux 入門 — Enforcing/Permissive、ラベル、トラブルシューティング
- #2 LVM — PV/VG/LV、スナップショット、拡張
- #3 ストレージ深化 — Stratis、NFS、Samba
- #4 ネットワーキング — NetworkManager (nmcli)、bonding、teaming
- #5 ログ管理 — journald、rsyslog、log rotation
- #6 ジョブスケジューリング — cron、systemd timer、at
- #7 コンテナ入門 — Podman/Buildah/Skopeo (Docker との違い) ← この記事
Docker 自体の入門は別シリーズで扱いました — Docker 基礎。この記事は Docker を 1 度でも使った読者を想定し、同じ作業を RHEL 9 の標準ツールでどうやるかに焦点を当てます。
なぜ RHEL 9 は Docker ではないのか #
CentOS/RHEL 8 から Red Hat は Docker パッケージをデフォルトリポジトリから外しました。その空いた役割を Podman が埋めました。理由はシンプルではありませんが運用観点で見ると明確な 1 行に要約されます。
Docker は root デーモンにすべてのコンテナ権限が集中する構造、Podman はデーモンなしでユーザー権限で直接実行される構造。
| 比較 | Docker | Podman |
|---|---|---|
| デーモン | dockerd 常時実行 | なし |
| コマンド → コンテナ | クライアントがデーモンに RPC | fork/exec 直接実行 |
| デフォルト権限 | root デーモン | ユーザー権限 (rootless デフォルト) |
| 攻撃表面 | デーモン 1 か所に集中 | コンテナ別に隔離 |
| systemd 統合 | 別途 wrapper が必要 | quadlet で 1 級市民 |
| compose | docker-compose | podman compose / quadlet |
| コマンド | docker ps | podman ps (alias で docker も可) |
コマンドが同じなので学習コストはほぼ 0 ですが、rootless がデフォルトという点 は運用観点から大きな違いを生みます。
Podman インストールと最初のコンテナ #
$ sudo dnf install -y podman
$ podman --version
podman version 4.9.xDocker ユーザーに馴染みのフローそのまま:
$ podman run --rm -it docker.io/library/alpine sh
/ # apk add curl
/ # exit
# nginx を立ち上げる
$ podman run -d --name web -p 8080:80 docker.io/library/nginx:1.27
$ curl http://localhost:8080
$ podman ps
$ podman logs web
$ podman stop web && podman rm web最初に見たらどこが違うかわからないほど。Docker と異なる点が露わになる箇所は「これを root なしでやった」 という事実です。
rootless コンテナ — 何が変わるか #
ユーザー名前空間 #
rootless Podman は ユーザー名前空間 (user namespace) の上で動作します。コンテナ内の root (uid 0) はホストでは一般ユーザーの uid にマッピングされます。コンテナ内では権限があるように見えてもホストリソースに対しては一般ユーザー分の権限しか持ちません。
$ id
uid=1000(curtis) gid=1000(curtis)
$ podman unshare cat /proc/self/uid_map
0 1000 1
1 100000 65536読み方: コンテナ内 uid 0 = ホスト uid 1000 (私)、コンテナ内 uid 1〜65536 = ホスト uid 100000〜165535 (subuid)。
このマッピングが可能になるには /etc/subuid と /etc/subgid にユーザー別領域が設定されていなければなりません。RHEL 9 はユーザー作成時に自動で掴みます。
$ cat /etc/subuid
curtis:100000:65536rootless 制約 #
権限が弱い分、できないこともあります。
- ポート 1024 未満のバインディング ✗ — 80、443 は sysctl で解放するか Caddy/HAProxy のような reverse proxy の後ろに置く
- NFS マウント ✗ — ユーザー名前空間で NFS は通常ブロックされます
- AF_NETLINK 制限 — ホストネットワークの奥深くに入るツールは動作しない可能性
- MTU/IP forwarding 一部オプション制限
このような限界に当たれば rootful モード (sudo podman ...) に逃げることができます。両者は完全に分離されたストレージを使います。
# rootless
~/.local/share/containers/storage/
# rootful
/var/lib/containers/storage/同じマシンに同じイメージを 2 度ダウンロードすることになるので、運用では片側に統一 するのが標準です。セキュリティが重要なら rootless、ホストネットワーク統合が絶対的なら rootful。
ポート 1024 未満のバインディング #
# 永続適用
$ sudo sh -c 'echo "net.ipv4.ip_unprivileged_port_start=80" > /etc/sysctl.d/99-podman-rootless.conf'
$ sudo sysctl --system
# その後
$ podman run -d -p 80:80 nginxイメージ — レジストリと OCI #
Podman は OCI (Open Container Initiative) 標準に従います。Docker イメージと互換です。ただしレジストリ名を 明示的に 書く点が異なります。
$ podman pull nginx:1.27
?: Please select an image:
▸ registry.access.redhat.com/nginx:1.27
registry.redhat.io/nginx:1.27
docker.io/library/nginx:1.27Docker は問わずに docker.io を仮定しますが、Podman は /etc/containers/registries.conf に書かれた候補のうちどこから受けるか 1 度確認します。自動化スクリプトなら常にフルパス (docker.io/library/nginx:1.27) で書くのが安全。
unqualified-search-registries = ["registry.access.redhat.com", "registry.redhat.io", "docker.io"]Red Hat レジストリ #
RHEL ユーザーがよく出会う 2 か所:
registry.access.redhat.com— 認証なしで受けられる公開イメージ (UBI など)registry.redhat.io— サブスクリプション認証が必要。podman login registry.redhat.ioでログイン後に使用
UBI (Universal Base Image) — Red Hat が RHEL ベースイメージを無料で配布するライン。運用で RHEL の上に RHEL 互換コンテナを回す標準です。
$ podman pull registry.access.redhat.com/ubi9/ubi:latest
$ podman run --rm -it registry.access.redhat.com/ubi9/ubi cat /etc/redhat-release
Red Hat Enterprise Linux release 9.x ...Buildah — Dockerfile なしでもビルド #
Podman は podman build で Dockerfile ビルドをサポートします (内部的に Buildah が働きます)。Buildah を直接使えばより細かく制御できます。
Dockerfile ビルド #
FROM registry.access.redhat.com/ubi9/ubi:latest
RUN dnf install -y python3 && dnf clean all
COPY app.py /opt/app.py
CMD ["python3", "/opt/app.py"]$ podman build -t myapp:1.0 .
$ buildah bud -t myapp:1.0 . # 同じ意味Containerfile があればそれを使い、なければ Dockerfile を使います。両方 OCI 標準形式。
Buildah スクリプトビルド #
Dockerfile は layer ごとに commit が起きてイメージが大きくなりやすい構造です。Buildah は buildah from → コンテナ開始 → 変更 → buildah commit の明示的フローを提供して layer 数を直接制御できます。
#!/bin/bash
ctr=$(buildah from registry.access.redhat.com/ubi9/ubi)
buildah run "$ctr" -- dnf install -y python3
buildah run "$ctr" -- dnf clean all
buildah copy "$ctr" app.py /opt/app.py
buildah config --cmd '["python3","/opt/app.py"]' "$ctr"
buildah commit "$ctr" myapp:1.0
buildah rm "$ctr"dnf install + dnf clean all を 2 つの RUN に分けても結果イメージには 1 layer として入ります。運用でイメージサイズを減らさなければならないときに有用な技法です。
Dockerfile でよく見る RHEL パターン #
FROM registry.access.redhat.com/ubi9/go-toolset:latest AS build
WORKDIR /src
COPY . .
RUN go build -o /tmp/app ./cmd/app
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
COPY --from=build /tmp/app /usr/local/bin/app
USER 1001
CMD ["/usr/local/bin/app"]ubi9/ubi-minimal は約 100MB レベルのスリムなベース。USER 1001 のように non-root ユーザーに落とすのがコンテナセキュリティの基本。
Skopeo — レジストリの間を移すツール #
skopeo はイメージを 解凍せずに レジストリの間で移したり検査できるツール。バックアップ、ミラーリング、エアギャップ (air-gapped) 環境に必須。
$ sudo dnf install -y skopeoよく使うフロー #
$ skopeo inspect docker://registry.access.redhat.com/ubi9/ubi:latest
{
"Name": "registry.access.redhat.com/ubi9/ubi",
"Digest": "sha256:abc...",
"Architecture": "amd64",
...
}$ skopeo copy \
docker://docker.io/library/nginx:1.27 \
docker://registry.example.com/mirror/nginx:1.27# インターネット接続されたところで
$ skopeo copy \
docker://docker.io/library/nginx:1.27 \
dir:/tmp/nginx-1.27
# tar で束ねて移した後
$ tar czf nginx-1.27.tar.gz -C /tmp nginx-1.27
# 隔離されたところで
$ tar xzf nginx-1.27.tar.gz -C /tmp
$ skopeo copy \
dir:/tmp/nginx-1.27 \
docker://registry.internal/mirror/nginx:1.27$ skopeo list-tags docker://registry.access.redhat.com/ubi9/ubidocker pull + docker save + docker load の組み合わせを 1 コマンドで終えると見ればよいです。デーモンが必要ないので CI/CD パイプラインでも軽く回ります。
Podman + systemd — quadlet #
運用でコンテナを立ち上げる標準的な方法は systemd unit として管理することです。RHEL 9 の Podman 4.4+ から quadlet が導入されて systemd フレンドリーな unit ファイルでコンテナを定義できます。
[Unit]
Description=Nginx web container
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/nginx:1.27
PublishPort=8080:80
Volume=/srv/web:/usr/share/nginx/html:ro,Z
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=multi-user.target default.targetsystemd に登録して開始:
$ sudo systemctl daemon-reload
$ sudo systemctl start web.service
$ sudo systemctl status web.service
$ journalctl -u web.service -fquadlet は .container ファイルを systemd が起動時点で自動で .service unit に変換してくれます。systemd がコンテナライフサイクルの 1 級管理者 になる構造です。
ユーザー単位でも同じく可能です — ~/.config/containers/systemd/web.container に置いて systemctl --user で管理。
Volume の :Z — SELinux ラベル
#
quadlet 例の Volume=/srv/web:/usr/share/nginx/html:ro,Z で Z はホストディレクトリに コンテナ専用 SELinux ラベル を自動で付けます。
| オプション | 意味 |
|---|---|
:z | 共有ラベル (複数のコンテナがアクセス可能) |
:Z | 専用ラベル (このコンテナのみアクセス) |
| オプションなし | ラベルが付かない → SELinux Enforcing 環境でコンテナが拒否される |
#1 SELinux 入門 で扱ったラベル概念がコンテナボリュームでもそのまま生きているという点。このオプションを抜かしてコンテナが読み取り拒否される事故が最もよくあります。
Podman compose — docker-compose 互換 #
既存の docker-compose.yml をそのまま使いたいなら:
$ sudo dnf install -y podman-compose
$ podman-compose -f docker-compose.yml up -dただし運用では quadlet に移すフローを推奨します — systemd 統合と起動時自動開始を一度に把握できます。
Auto Update — イメージ自動更新 #
quadlet や systemd unit に AutoUpdate=registry を書いておけば、podman-auto-update.timer (RHEL 9 デフォルト timer の 1 つ) が毎日 1 度新しいイメージを確認して自動でダウンロードしてコンテナを再起動します。
$ systemctl list-timers podman-auto-update.timer
$ podman auto-update --dry-run運用推奨:
- 開発/ステージング:
AutoUpdate=registryで自動更新 - プロダクション: 自動更新を切って明示的なデプロイ手順で (意図しないマイナーバージョン変更を遮断)
デバッグ — コンテナが立ち上がらないとき #
# 1. イメージが実際にダウンロードされているか
$ podman images
$ podman pull <image>
# 2. コンテナ試行時の正確なエラー
$ podman run --rm -it <image> sh
# (バックグラウンド -d で立ち上げて「Exited」だけ見て終わることが多い)
# 3. 生きているコンテナのログ
$ podman logs <name>
$ podman logs --tail 100 -f <name>
# 4. SELinux 拒否 — Volume に :Z を抜かしたとき
$ sudo ausearch -m AVC -ts recent
$ sudo journalctl -t setroubleshoot --since "10 min ago"
# 5. ポート衝突
$ sudo ss -tlnp | grep :8080
# 6. quadlet — 変換結果確認
$ /usr/libexec/podman/quadlet -dryrun-d で立ち上げた途端コンテナが死んで消えるなら、--rm オプションを外して podman logs <name> または podman run --rm -it <image> sh でインタラクティブに入ってみるのが最速の診断です。
よくある落とし穴 #
- Docker 式の習慣 —
docker.io/library/接頭を抜かすと unqualified-search 候補のうちどこから受けるか問われたり別のイメージを受けることになります。自動化では常にフルパスを使ってください。 - rootless で 80 ポートを試行 —
bind: permission denied。sysctl で解放するか reverse proxy の後ろへ。 - Volume
:Z漏れ — SELinux Enforcing 環境 (RHEL 9 デフォルト) でコンテナがホストディレクトリを読めません。 - rootless ↔ rootful ストレージ分離 —
sudo podman pullで受けたイメージは一般podmanコマンドからは見えません。片側に統一。 - コンテナ内 root = ホスト root ではない — rootless は user namespace でマッピングされる。セキュリティ前提が異なることを忘れないでください。
- イメージキャッシュの蓄積 —
podman system prune -a -fで周期的整理。quadletAutoUpdate=registryだけ使うと古いイメージが積み重なり続けます。
覚えておくコマンド #
| 作業 | コマンド |
|---|---|
| イメージ受け / 確認 | podman pull <img> / podman images |
| コンテナ実行 | podman run -d --name <n> -p 8080:80 <img> |
| ログを見る | podman logs -f <n> |
| シェル進入 | podman exec -it <n> bash |
| イメージビルド | podman build -t <tag> . |
| レジストリコピー | skopeo copy docker://A docker://B |
| イメージ検査 | skopeo inspect docker://<img> |
| systemd 統合 | quadlet *.container + systemctl daemon-reload |
| 整理 | podman system prune -a |
まとめ #
- Podman = Docker コマンド互換 + デーモンなし + rootless デフォルト。RHEL 9 のコンテナ標準。
- Buildah — イメージビルドツール。
podman buildが内部で使用。Dockerfile/Containerfile + スクリプトビルド両方サポート。 - Skopeo — レジストリの間を移すツール。ミラーリング、エアギャップ、CI/CD パイプラインに必須。
- quadlet — Podman + systemd の 1 級統合。
.containerファイルを systemd が自動で service に変換。 - 運用の要となる落とし穴: フルレジストリパス、Volume の
:Z、rootless ↔ rootful 分離、コンテナ root とホスト root の違い。
シリーズ締めくくり #
これで RHEL 中級シリーズ 7 編を終えます。SELinux、LVM、ストレージ、ネットワーキング、ログ、スケジューリング、コンテナまで、運用の日常でよく出会う 7 領域を一巡しました。
次は RHEL 上級 シリーズです。クラスタリング (Pacemaker)、高可用性、チューニング、セキュリティ強化、Ansible 自動化のような 1 マシンを越えて複数マシンを一緒に運用する領域に移ります。
ここまで読んでいただきありがとうございました。