Rootless Podman + Quadlet によるコンテナ運用基盤の構築: UID マッピング・権限設計・macOS DNS 問題の解決
Ubuntu 24.04のPodman 4.9.xでQuadletを用いたsystemdサービス化、Rootless環境のUIDマッピングと権限設計(Lokiの実例)、macOSでPodman DesktopとCLIのDNS差異に対処した全記録。
結論
ローカル開発基盤のコンテナ運用は、ホストごとに以下の方針で確定した。
| ホスト | OS | Podman モード | サービス管理 |
|---|---|---|---|
| Compute Server | Ubuntu 24.04 | rootless | Quadlet(.container → systemd) |
| Storage Server | Ubuntu 24.04 | rootless | Quadlet(.container → systemd) |
| Desktop PC | macOS | Podman Desktop | compose(GUI 管理) |
Linux 2 台は rootless Podman + Quadlet で systemd サービス化するのが基本形になった。Mac だけは CLI(podman machine)を試したが、storage.home.arpa の名前解決がコンテナ内から通らず、結局 Podman Desktop に落ち着いた。
構築の過程で Quadlet の動作確認、UID マッピングの理解、Loki の権限問題解決、podman unshare と sudo の使い分けをそれぞれ整理した。以下はその記録である。
Quadlet 検証: Podman 4.9.x で動くか
Ubuntu 24.04 の標準パッケージ(Podman v4.9.3)には podman quadlet サブコマンドが含まれていない。しかし Quadlet 機能自体は systemd-generator 経由で完全に動作する。
確認方法
所定のディレクトリに .container ファイルを配置すると、.service が自動生成される。
systemctl cat node-exporter.service
# /run/systemd/generator/node-exporter.service
# Automatically generated by /usr/lib/systemd/system-generators/podman-system-generator
[Unit]
Description=Prometheus Node Exporter (Quadlet)
After=network-online.target
Wants=network-online.target
SourcePath=/etc/containers/systemd/node-exporter.container
...
Node Exporter の Quadlet ファイル例
ファイルパス: /etc/containers/systemd/node-exporter.container
[Unit]
Description=Prometheus Node Exporter (Quadlet)
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/prom/node-exporter:v1.10.1
ContainerName=node-exporter
Network=host
ReadOnly=true
DropCapability=ALL
Volume=/:/host:ro,rslave
Args=--path.rootfs=/host
[Install]
WantedBy=multi-user.target
配置後:
systemctl daemon-reload
systemctl start node-exporter.service
systemctl status node-exporter.service
ENTRYPOINT の罠
初回は Exec=/bin/node_exporter --path.rootfs=/host と書いて失敗した。prom/node-exporter イメージはデフォルトで /bin/node_exporter が ENTRYPOINT に設定されているため、Exec で同じコマンドを指定すると衝突する。ENTRYPOINT が設定済みのイメージに引数を渡す場合は Args を使う。
systemd 連携の注意点
/run/systemd/generator下に動的生成された Unit はsystemctl enableできない[Install]セクションのWantedBy=multi-user.targetで自動起動を制御する
Rootless UID マッピングの理解
subuid による「所有権の壁」
Rootless Podman の安全性は、ホストの一般ユーザー(UID 1000)がコンテナ内で使われるサブ UID(100000〜)のファイルにアクセスできないことで成り立っている。これはバグではなく設計どおりの隔離機能である。
ホスト上で所有者が 100999:100999 に見えるファイルは、ksh3(UID 1000)からは読み書きも権限変更もできない。これらを操作するには podman unshare でユーザー名前空間に入り、内部の仮想 root として操作するのが正当な手順になる。
Loki の permission denied: 実例と分析
Grafana Loki(grafana/loki:3.5)を rootless コンテナとして起動した際、以下のエラーが発生した。
open /loki/index_cache/... .tsdb-tmp: permission denied
ホスト側のデータディレクトリ /opt/containers/runtime/loki/data は ksh3:1000 が所有、モード 0755。Loki はイメージ定義の loki ユーザー(UID=10005)で実行される。rootless の UID マッピングを経由すると、コンテナの実行 UID がホスト所有者と一致せず、書き込み権限が得られない。
UID マッピングフロー
問題の本質を理解するには、ケースを並べるのが一番早い。
ホスト: ksh3 (UID=1000)
Case A: keep-id なし (イメージ loki=10005)
container root(0) -> host 100000 (subuid開始例)
container loki(10005) -> host 110005
実行UID = 110005 != ディレクトリ所有 1000 → 書けない
Case B: keep-id + loki(罠)
container root(0) -> host 1000
container loki(10005) -> host 110005
実行UID = 110005 != 1000 → 書けない
Case C: keep-id + loki + :U
:U によりホスト側 /data の所有者 chown → 110005
実行UID = 110005 → 書ける
しかしホストユーザー1000は所有者でなくなる → 編集しにくい
Case D: keep-id + User=0 ← 採用
container root(0) -> host 1000
実行UID = 1000 → 所有者一致 → 書ける (ホスト操作性維持)
Case B が罠で、keep-id を付けても Loki ユーザーはサブ UID 側に残る。keep-id はコンテナ内 root のマッピングを変えるだけで、実行ユーザーは変わらない。
選択肢比較
| 方法 | 仕組み | 動く | ホスト編集 | 最小権限 |
|---|---|---|---|---|
| User=0 + keep-id | root をホスト UID に | OK | OK | 低 |
| loki + ACL | loki UID に ACL 付与 | OK | OK | 中 |
| loki + :U | 自動 chown (所有者 loki) | OK | 低 (sudo 要) | 中 |
| 0777 | 全開放 | OK | OK | 非推奨 |
推奨パターン
- 速く動かしたい / 開発用途 →
UserNS=keep-id+User=0 - 非 root 実行を維持したい → ACL 方式(
UserNS=keep-id不要、User=指定なし)
Loki Quadlet 設定例(User=0 + keep-id 方式)
[Unit]
Description=Grafana Loki (Quadlet - rootless)
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/grafana/loki:3.5
ContainerName=loki
Exec=-config.file=/etc/loki/config.yaml
UserNS=keep-id
User=0
Network=host
PublishPort=3100:3100
Volume=/opt/containers/etc/loki:/etc/loki:ro
Volume=/opt/containers/runtime/loki/data:/loki:rw
Volume=/opt/containers/etc/loki/certs:/etc/certs:ro
[Service]
Restart=always
TimeoutStartSec=900s
[Install]
WantedBy=multi-user.target
systemctl --user daemon-reload
systemctl --user restart loki.service
podman logs loki | grep -i permission || echo "permission errors none"
ACL 方式(非 root 維持)
LOKI_UID=$(podman run --rm docker.io/grafana/loki:3.5 id -u)
LOKI_GID=$(podman run --rm docker.io/grafana/loki:3.5 id -g)
setfacl -m u:${LOKI_UID}:rwx /opt/containers/runtime/loki/data
setfacl -R -m u:${LOKI_UID}:rwx /opt/containers/runtime/loki/data
setfacl -d -m u:${LOKI_UID}:rwx /opt/containers/runtime/loki/data
systemctl --user restart loki.service
podman logs loki | grep -i permission || echo "permission errors none"
運用モデル: podman unshare と sudo の3段階
Rootless の隔離は「所有権でブロックしているだけ」であり、sudo を使えばカーネルレベルで全制限が解除される。これを「セキュリティの敗北」ではなく「正しい例外処理」として位置づけるのが、実用的な運用の鍵になる。
| フェーズ | 操作 | 方法 |
|---|---|---|
| 通常時 | 一般ユーザーとして操作、コンテナ内ファイルには「触れない」 | Rootless のデフォルト |
| 調整時 | コンテナ内ファイルの微調整、パーミッション変更 | podman unshare |
| 全権が必要な時 | ディレクトリ移設、バックアップ一括操作 | sudo |
バックアップのような純読み込み操作では、podman unshare より sudo rsync -a の方がシンプルで確実なケースもある。MinIO のメタデータ(.minio.sys)のように壊したくないデータの複製には、所有者情報を維持したまま -a オプションで複製するのが安全だった。
macOS の DNS 問題: CLI から Podman Desktop へ
Mac(Desktop PC)では当初 CLI podman machine を使っていた。しかし Grafana コンテナから storage.home.arpa(Storage Server)の Prometheus / Loki に到達できない問題が発生した。
症状
- ホストの Mac からは
curl https://storage.home.arpa:9090が通る - Grafana コンテナ内では名前解決エラーまたは
connection refused - DNS ログに
storage.home.arpa AAAA -> name does not exist
原因: Podman Desktop と CLI の名前解決の違い
| 観点 | Desktop | CLI (machine) |
|---|---|---|
/etc/hosts | ホストと共有 | 独立(VM 内で別管理) |
| DNS 設定反映 | 自動 | 手動 |
| ホスト汚染リスク | 高 | 低 |
| サンドボックス性 | 弱 | 強 |
CLI podman machine は完全に独立した VM として振る舞い、ホストの DNS / hosts を共有しない。home.arpa のようなローカルドメインがホスト側の設定に依存している場合、コンテナからは引けない。Compose の dns: 設定も VM 側の /etc/resolv.conf が独自生成されるため、素直に伝播しなかった。
解決策と判断
Linux サーバー側では Quadlet + rootless で確立した運用モデルがあり、Mac だけが DNS の問題を抱えていた。CLI の隔離モデルは堅牢だが、Mac は開発の起点(Grafana UI、ブラウザ、VSCode)であり、内部 LAN サービスへの到達性が最優先だった。
結局、Mac は Podman Desktop を採用した。Desktop はホスト側の名前解決を透過的に VM に持ち込むため、storage.home.arpa がそのままコンテナ内から引ける。加えて extra_hosts で固定マッピングも追加し、DNS の曖昧さに依存しない構成にした。
services:
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
- NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,*.home.arpa
volumes:
- /Users/ksh3/containers/iac/grafana/data:/var/lib/grafana
- /Users/ksh3/containers/iac/grafana/provisioning:/etc/grafana/provisioning:ro
- /Users/ksh3/containers/iac/grafana/certs:/etc/ssl:ro
extra_hosts:
- "storage.home.arpa:10.10.10.3"
depends_on:
- prometheus
注意事項
- Podman 4.9.x には
podman quadletCLI がないが、systemd-generatorが機能するので運用上は問題ない。Podman 5.x で CLI サポートが追加される User=0 + keep-idは開発用途では最短だが、コンテナ内 root 実行なので最小権限ではない。本番寄せなら ACL 方式を検討:U方式はホスト側の所有者がコンテナ UID に変わるため、開発時の操作性が大きく下がる- SELinux 有効環境では Volume に
:Z/:zを追加検討 - NFS /
root_squash環境では:Uやchownが失敗することがある - Podman Desktop は便利だが「安全なサンドボックス」ではなく「便利なブリッジ環境」。名前解決は自分で明示した方が確実
