1. Docker Composeでは誰も気にしていなかった「ボリュームの持ち主」

Docker Composeでローカル開発をしているとき、ボリュームの「所有者が誰か」を意識したことがある人は少ないはずだ。docker-compose.ymlにvolumes: ./data:/app/dataと書けば、ホストのディレクトリがそのままコンテナに見える。権限で困ることはほとんどない。
これは当然の結果で、Docker公式ドキュメントも「bind mountはデフォルトでホスト上のファイルへの書き込みアクセス権を持つ」と説明している(Docker公式ドキュメント: Bind mounts)。つまりホスト側のディレクトリ権限がそのままコンテナ内に反映されるだけで、Docker自身が別途権限レイヤーを挟むわけではない。開発用に広めの権限でディレクトリを作っておけば、たいていのアプリケーションは何も考えずに動く。
この「何も考えずに動く」という前提こそが、Docker ComposeからK3sへ移行するときに最初につまずく原因になる。K3s(あるいはKubernetesクラスタ全般)は、ボリュームの扱いに関してComposeとは根本的に異なる設計思想を持っているからだ。構築を手作業で詰めていくほど、こうした前提の差異に一つずつ気づいて直す作業が増えていく。
2. K3sに持ち込んだ瞬間、何が起きるか — local-path-provisionerの仕様

K3sには標準でRancherのlocal-path-provisionerが組み込まれており、PVC(PersistentVolumeClaim)を作成するだけでノードのローカルディスク上にボリューム用ディレクトリを自動生成してくれる(K3s公式ドキュメント: Volumes and Storage)。デフォルトではstorageClassName: local-pathが使われ、ボリュームの実体は/var/lib/rancher/k3s/storage配下に作られる。
問題は、このプロビジョニング処理がディレクトリの所有者(chown)までは面倒を見ないことだ。local-path-provisioner公式リポジトリのセットアップスクリプトを見ると、実行される処理はmkdir -m 0777 -p "$VOL_DIR"のみで、パーミッションは777(誰でも読み書き可能)に設定されるが、所有ユーザー・グループの変更(chown)は行われない(rancher/local-path-provisioner、k3s-io/k3s local-storage.yaml)。
777権限なら誰でも書き込めるはずだと思うかもしれない。ところが、多くの公式コンテナイメージはrunAsNonRootや特定のUID/GIDを前提に作られており、Pod側のsecurityContextでrunAsUserを明示していたり、イメージ自体が非rootユーザーで起動する設計になっていたりすると、ノード側のSELinux設定やumaskの組み合わせによって書き込みに失敗するケースが報告されている(k3s-io/k3s Issue #10130)。Docker Composeのbind mountでは起きなかった「誰が書き込むか」という問題が、K3sでは一気に表面化する。
3. 相対パスも罠になる — hostPathは絶対パス必須

Docker Composeのbind mountに慣れていると、もう一つ見落としがちな前提がある。Composeのvolumes:はdocker-compose.ymlからの相対パスを許容するが、KubernetesのhostPathボリュームは絶対パスでなければ動かない(Kubernetes公式ドキュメント: Volumes)。
volumes:
- name: data-volume
hostPath:
path: /mnt/data # 絶対パス必須。相対パスは不可
type: Directory
Compose時代の感覧のまま./dataのような相対パス表記をマニフェストに持ち込むと、Kubernetesはそのディレクトリをホスト上のどこにも見つけられずPodがCreateContainerConfigError等で止まる。さらにhostPathはノードのファイルシステムに直接アクセスできる強い権限を持つため、Kubernetes公式ドキュメントも本番環境では必要最小限のディレクトリに限定し、可能であればreadOnly: trueを付けることを推奨している。実際の移行では、local-path-provisionerが自動生成するPVC経由のボリュームに寄せて、hostPathを直接書く場面はできるだけ減らすのが安全だ。
4. 回避策 — fsGroupと initContainer、StorageClassのカスタムパターン

この権限問題には、複雑なスクリプトを書かなくても対処できる公式の仕組みがいくつか用意されている。
securityContext.fsGroupを設定する: PodのsecurityContextにfsGroupを指定すると、Kubernetesはコンテナ起動前にボリュームのグループ所有権を自動的にそのGIDへ変更してくれる(Kubernetes公式ドキュメント: Security Context)。runAsUserとfsGroupを合わせて設定しておけば、非rootコンテナでもボリュームへの書き込みが可能になる。初期構成で最初に試すべき最も手軽な方法だ。- StorageClassの
userPattern/groupPattern/permPatternをカスタマイズする:local-path-provisionerはStorageClassの設定パラメータでディレクトリ作成時の所有者・パーミッションパターンを変更できる仕組みを持っている(rancher/local-path-provisioner)。チーム全体で統一したUID/GIDを使う場合はこちらが向いている。 - initContainerでchownする: fsGroupが効かない特殊なケース(一部のCSIドライバやレガシーイメージ)では、root権限のinitContainerで対象ディレクトリを明示的にchownしてから本体コンテナを起動する方法が依然として有効だ。
どの方法を選ぶにせよ、「Docker Composeでは意識しなかったボリュームの所有者」を、K3sでは明示的に設計し直す必要があるという点は変わらない。この手の「fsGroupにするかStorageClassのパターンを変えるか」という判断は、経験がないと地味に時間を取られるポイントでもある。KuboのAI-Driven Deploymentであれば、アプリの構成要件を伝えるだけでこうしたボリューム権限の初期設計を提案してもらえるため、ゼロから手探りで検証する時間を減らせる。
5. depends_onも消える — 起動順を前提にしたComposeはそのままでは動かない

ボリューム権限と並んで、移行時にもう一つ見落とされがちなのが起動順序だ。Docker Composeのdepends_onはコンテナの起動順序を保証する便利な機能だが、Kubernetesには同等の概念が存在せず、Podは基本的に並列に起動する。
Compose定義を機械的にKubernetesマニフェストへ変換するツールkomposeを使った場合でも、depends_onキーは単純に無視される(kompose公式: conversion doc、OneUptime: Komposeによる変換ガイド)。DBコンテナより先にアプリコンテナが起動し、接続エラーでクラッシュループするという事態が、ボリューム権限の問題を解決した直後に次の壁として立ちはだかることが多い。
対策としては、initContainersでDB等の依存先に接続できるまで待機するロジック(until nslookup db-service; do sleep 2; doneのようなチェック)を入れるか、アプリケーション側に接続リトライを実装するのが定石だ(10 Common Pitfalls When Migrating Docker Compose to Kubernetes)。サービス名による名前解決自体はDocker ComposeのDNSとKubernetesのCoreDNSで概念的に近いため、ここは比較的移行しやすい部分だと言える(Service Discovery and DNS in Containers)。
こうした「ローカルでは暗黙の前提として機能していたものが、K3sでは明示的な設計を要求される」という構造は、ボリューム権限と起動順序の両方に共通している。
まとめ
Docker ComposeからK3sへの移行でつまずくポイントを整理すると、次の3点に集約される。
- ボリューム権限:
local-path-provisionerはディレクトリを777で作成するだけでchownしない。fsGroupやStorageClassのカスタムパターンで明示的に所有者を設計する必要がある。 - パス形式: Composeの相対パスはK3sの
hostPathでは通用しない。絶対パス指定が必須。 - 起動順序:
depends_onに相当する機能はKubernetesになく、initContainerやアプリ側リトライで代替する必要がある。
いずれも「ローカルで動いていたから本番でも動くはず」という前提を崩される落とし穴だ。こうした初期構築のハマりどころは、構成をゼロから手作業で詰めていく限り何度でも再発する。KuboのAI-Driven Deploymentなら、ボリュームの権限設計やStorageClassの選定を自然言語の指示で進められ、YAMLを1行ずつ書きながら権限エラーの原因を切り分けるような作業を減らせる。構成の全体像はKubo Captain UIで可視化されるため、ブラックボックスのまま進めてしまう不安もない。
Docker Composeからの移行で手戻りを減らしたい、あるいは今まさにPermission deniedで止まっているという場合は、お問い合わせから構成相談も可能だ。K3sベースのKuboは、EKS/AKSのパワーをVercelのような手軽さで使える環境を目指しており、今回挙げたような移行時の初期ハマりどころを前提にした設計になっている。社内データを外に出せない事情があるなら、同じK3sベースで自社環境に閉じて構築できるKubo On-Premiseという選択肢もある。