Rootless Podmanへ移す前に、止まる場所を先に見ておく
小さなWebサービスや管理ツールをコンテナで動かしていると、それらをずっとrootで動かし続けることが少し気になり始めます。Rootless Podmanはその不安に対する一つの答えに見えますが、「rootlessだから安全」という言葉だけで常用環境を置き換えると、たいていポート公開とファイル権限のところで手が止まります。
この記事では、安全性の美談としてrootlessを語る前に、Docker Composeに慣れた運用者が最初に踏む段差を、使い捨ての検証用VM上で実コマンドと実ログとして記録します。確認したいのは次の4点です。
- rootless状態はどのコマンドで確認できるのか
- 低番ポート(<1024)で失敗したとき、何が表示されるのか
- ホスト側ボリューム権限の失敗はどう見えるのか
- rootful Dockerから置き換える前に、何を小さく試しておくべきか
検証環境
検証は、使い捨てのエフェメラルVM上で行いました。常用環境ではなく、終わったら破棄する前提の隔離環境です。
- ディストリビューション: Debian 12 (bookworm) 系
- Podman: version 4.3.1
- 関連パッケージ: uidmap / slirp4netns / fuse-overlayfs / curl
- 実行ユーザー: root権限で検証用一般ユーザー
labuserを作成し、以降のコンテナ操作はすべてlabuserで実施
Podmanの導入とユーザー名前空間(subuid/subgid)の設定にはパッケージ導入とカーネル機能が必要なため、ネットワークやパッケージ導入が制限された環境ではなく、導入可能な使い捨てVMを使っています。
実行コマンドと実行ログ
1. 導入と検証用ユーザーの用意
まずPodmanと依存パッケージを導入し、検証用の一般ユーザー labuser にsubuid/subgidの範囲を割り当てます。
# export 設定項目=noninteractive
# apt-get update && apt-get install -y podman uidmap slirp4netns fuse-overlayfs curl
# useradd -m -s /bin/bash labuser
# usermod --add-subuids 100000-165535 --add-subgids 100000-165535 labuser
# loginctl enable-linger labuser
# grep labuser /etc/subuid /etc/subgid; id labuser
割り当て後の確認結果は次のとおりです。
/etc/subuid:labuser:231072:65536
/etc/subgid:labuser:231072:65536
uid=1002(labuser) gid=1002(labuser) groups=1002(labuser)
2. rootless状態の確認
まず「本当にrootlessで動いているか」を確認します。Podmanはここを明示的に答えてくれます。
# su - labuser -c "podman --version && podman info --format '{{.Host.Security.Rootless}}'"
podman version 4.3.1
true
podman info の Host.Security.Rootless が true であること、これがrootless運用の入口です。さらに名前空間のマッピングも見ておきます。
# su - labuser -c "id; podman unshare cat /proc/self/uid_map"
uid=1002(labuser) gid=1002(labuser) groups=1002(labuser)
0 1002 1
1 231072 65536
コンテナ内のUID 0(root)が、ホスト側ではUID 1002(labuser)に対応しています。コンテナ内のroot以外のUIDは231072以降にマッピングされます。後で出てくるボリューム権限の失敗は、ここのズレが原因になります。
3. 高番ポートでの正常起動(成功例)
先に成功する例を押さえます。1024以上のポート(ここでは8080)でnginxを起動し、HTTP到達を確認します。
# su - labuser -c "podman run -d --name web-high -p 8080:80 docker.io/library/nginx:alpine; sleep 3; curl -s -o /dev/null -w '設定項目=%{http_code}\n' http://127.0.0.1:8080"
db61b80d99fe29ac2f2ef2e9a9be1aa28f5a4b24a601ad48ca18ad6b5d3137fe
設定項目=200
起動し、設定項目=200 が返りました。ここまではDockerと同じ感覚で進みます。
4. 低番ポート公開の失敗(失敗1)
次に、同じ構成のままポートだけを80(<1024)に変えます。Dockerでは何気なく書く設定です。
# su - labuser -c "podman run -d --name web-low -p 80:80 docker.io/library/nginx:alpine"
ここで止まります。終了コードは126で、メッセージは次のとおりです。
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied
失敗の理由と回避策がそのまま書かれているのが、このログの良いところです。要点は2つです。
- 一般ユーザーは既定で1024未満の特権ポートをバインドできない(
bind: permission denied) - 回避するなら、
net.ipv4.ip_unprivileged_port_startを下げてホスト側で特権ポートの開始位置を変えるか、1024以上のポートを使う
つまり「80番でそのまま公開」をしたい場合、リバースプロキシで80を受けて内部は高番ポートへ回す、もしくはホスト側のカーネル設定に手を入れる、という判断が先に必要になります。
5. ホスト側ボリューム権限の失敗(失敗2)
もう一つの段差は永続化データです。root所有・パーミッション700のディレクトリをホスト側に作り、それをコンテナへマウントして書き込みを試します。
# mkdir -p /tmp/issue165-rootdata && chown root:root /tmp/issue165-rootdata && chmod 700 /tmp/issue165-rootdata
# su - labuser -c "podman run --rm -v /tmp/issue165-rootdata:/data docker.io/library/alpine sh -c 'echo test > /data/probe.txt'"
結果は終了コード1で、書き込みに失敗します。
sh: can't create /data/probe.txt: Permission denied
コンテナ内のrootは、ホスト側ではlabuser(UID 1002)として振る舞います。root所有で700のディレクトリには、labuserは書き込めません。Dockerをrootで動かしていたときは「コンテナのrootがホストのrootとして書ける」感覚だったため、同じディレクトリ設計のまま移すと、ここで原因不明の保存失敗に見えます。
対処の方向は、マウント先のディレクトリ所有者・パーミッションをlabuser(または名前空間でマッピングされるUID)に合わせること、もしくは名前付きボリュームを使ってPodman側に管理を委ねることです。
6. rootful Dockerとの最小対比
参考として、同じ80番ポートをrootful Dockerで公開すると通る点だけを最小コマンドで確認しました。
# docker run -d --name dock-low -p 80:80 nginx:alpine && sleep 3 && curl -s -o /dev/null -w '設定項目=%{http_code}\n' http://127.0.0.1:80
# docker rm -f dock-low; docker rmi nginx:alpine
設定項目=200
rootful Dockerでは80番がそのまま通ります。これはDockerが安全という意味ではなく、「rootで動くから特権ポートをそのまま開けている」というだけです。rootless化で失われるのはこの暗黙の特権であり、それが今回の失敗1の正体です。
7. ログの読みやすさ
失敗の原因追跡には podman logs がそのまま使えます。高番ポートで起動したコンテナのログには、起動とアクセスがDockerと同じ形式で記録されていました。
2026/06/25 13:33:52 [notice] 1#1: nginx/1.31.2
...
<アクセス元> - - [25/Jun/2026:13:33:55 +0000] "GET / 設定項目/1.1" 200 896 "-" "curl/7.88.1" "-"
失敗側(低番ポート)はコンテナが起動に至っていないため、ログは空でした。失敗はコンテナのログではなく、podman run の標準エラー出力側に出る、という点も覚えておくと迷いません。
8. ロールバック
最後に、コンテナとイメージをまとめて削除し、環境を元に戻せることを確認しました。
# su - labuser -c "podman rm -f web-high web-low; podman rmi -a -f; podman system prune -a -f; podman ps -a; podman images"
Total reclaimed space: 0B
--- ps ---
設定項目 ID 設定項目 設定項目 設定項目 設定項目 設定項目 設定項目
--- images ---
設定項目 TAG 設定項目 ID 設定項目 設定項目
podman ps -a と podman images がどちらも空になり、rootless環境はユーザー空間内で完結して片付きました。常用ではなく、まず使い捨て環境で試して破棄する、という流れがそのまま取れます。
結果のまとめ
比較の軸ごとに整理すると、判断材料は次のようになります。
| 評価軸 | rootful Docker | Rootless Podman | 一般ユーザー単体systemd |
|---|---|---|---|
| 起動権限 | rootで動作 | 一般ユーザーで動作(Rootless=true) | 一般ユーザーで動作 |
| 低番ポート公開 | そのまま可(80でも200) | 既定では不可、要カーネル設定か高番ポート | 既定では不可(同じ制約) |
| 永続化データ | ホストrootと同一視で書けることが多い | UID名前空間のマッピングに権限を合わせる必要あり | プロセスのUIDに権限を合わせる |
| 失敗時のログ | docker logs / 標準エラー | podman logsと標準エラー、メッセージに回避策が含まれる | journalctl 等 |
| ロールバック | docker rm/rmi | podman system pruneでユーザー空間内に完結 | サービス停止・無効化 |
結論:置き換える前に、この2点を自分のサービスで試す
今回の検証から言えるのは、「Rootless Podman=安全だから全部置き換えられる」ではない、ということです。rootless化で得られるのは権限の縮小ですが、その代償として、これまでrootで動かしていたことに暗黙に依存していた2点が表に出てきます。
Rootless Podmanへ移してよい条件は、おおむね次のように言えます。
- 公開ポートを1024以上に寄せられる、もしくはリバースプロキシやカーネル設定で80/443を扱う構成を許容できる
- 永続化データのディレクトリ所有者・パーミッションを、コンテナ内UIDの名前空間マッピングに合わせて設計し直せる(または名前付きボリュームに寄せられる)
- まず使い捨て環境で起動・書き込み・削除まで通してから、常用サービスを移す手順を取れる
逆に、「80番で直接公開し続けたい」「既存のroot所有ディレクトリ構成を一切変えたくない」という状態のままなら、移行はポートと権限のところで必ず一度止まります。その場合は、急いで全置換するより、対象を1サービスに絞って高番ポート+ボリューム権限の見直しから小さく試すのが、いちばん安全な進め方です。
安全性は、起動が成功したかどうかではなく、失敗が予測でき、戻せるかどうかで測れます。今回のように「どこで止まるか」を先に見ておくことが、その判断の材料になります。
参考
- Podman Documentation: https://podman.io/docs
- Podman rootless tutorial: https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md
- podman-run(1): https://docs.podman.io/en/latest/markdown/podman-run.1.html
この検証を回している環境
この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。
ラボ構成のまとめを見る →