結論

ローカル開発基盤のコンテナ運用は、ホストごとに以下の方針で確定した。

ホストOSPodman モードサービス管理
Compute ServerUbuntu 24.04rootlessQuadlet(.container → systemd)
Storage ServerUbuntu 24.04rootlessQuadlet(.container → systemd)
Desktop PCmacOSPodman Desktopcompose(GUI 管理)

Linux 2 台は rootless Podman + Quadlet で systemd サービス化するのが基本形になった。Mac だけは CLI(podman machine)を試したが、storage.home.arpa の名前解決がコンテナ内から通らず、結局 Podman Desktop に落ち着いた。

構築の過程で Quadlet の動作確認、UID マッピングの理解、Loki の権限問題解決、podman unsharesudo の使い分けをそれぞれ整理した。以下はその記録である。


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/dataksh3: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-idroot をホスト UID にOKOK
loki + ACLloki UID に ACL 付与OKOK
loki + :U自動 chown (所有者 loki)OK低 (sudo 要)
0777全開放OKOK非推奨

推奨パターン

  1. 速く動かしたい / 開発用途UserNS=keep-id + User=0
  2. 非 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 の名前解決の違い

観点DesktopCLI (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 quadlet CLI がないが、systemd-generator が機能するので運用上は問題ない。Podman 5.x で CLI サポートが追加される
  • User=0 + keep-id は開発用途では最短だが、コンテナ内 root 実行なので最小権限ではない。本番寄せなら ACL 方式を検討
  • :U 方式はホスト側の所有者がコンテナ UID に変わるため、開発時の操作性が大きく下がる
  • SELinux 有効環境では Volume に :Z / :z を追加検討
  • NFS / root_squash 環境では :Uchown が失敗することがある
  • Podman Desktop は便利だが「安全なサンドボックス」ではなく「便利なブリッジ環境」。名前解決は自分で明示した方が確実