日常CLIコンテナでhost UID/GIDに合わせてshellへ入る設計

日常CLIコンテナでhost UID/GIDに合わせてshellへ入る設計

目的

日常的に使う CLI tool をコンテナ image にまとめると、host の環境を壊さずに新しい tool や最新版を試せる。 一方で、作業ディレクトリを bind mount して shell に入る場合、コンテナ内ユーザーの UID/GID が host とずれると、作成ファイルの所有者が壊れる。

Dockerfile に個人の user name、UID、GID を焼かず、起動時に host user に合わせる。

shell wrapperとentrypointを分ける

責務を分ける。

shell wrapper:
  - host側で実行する
  - container runtimeを選ぶ
  - bind mountを組み立てる
  - hostのUID/GID/USER/HOMEを環境変数で渡す
  - containerは一度rootで起動する

entrypoint:
  - container内で実行する
  - hostと同じUID/GIDのuser/groupを作る
  - 必要な補助groupを設定する
  - 作業userへ権限を落としてcommandを実行する

この形にすると、image 自体は汎用のままにできる。 利用者ごとの UID/GID は runtime の入力として扱う。

なぜdocker exec –userだけにしないか

docker exec --user <uid>:<gid> で入るだけだと、container 内の /etc/passwd/etc/group と整合しない。 特に supplementary groups が反映されないと、serial device、Docker socket、containerd socket のような group permission に依存する対象で詰まりやすい。

entrypoint で user/group を作り、対話 shell では setpriv --init-groupsgosu のような tool で group database に基づいて降りる。

root entrypoint
  -> create group if needed
  -> create user with host UID/GID
  -> adjust supplementary groups
  -> setpriv --init-groups --reuid <uid> --regid <gid>
  -> shell

既存UIDとの衝突

base image に同じ UID の user が存在することがある。 この場合、UID が同じなら file ownership としては同一人物に見えるが、login name や HOME が想定と違うと shell 体験が壊れる。

方針:

  • host user name を優先して作る
  • 同じ UID が既にある場合は、必要に応じて non-unique user を許容する
  • HOME は bind mount した host home または専用 workspace に寄せる
  • container 内の既存 service user を壊さない

日常 CLI image では、daemon を常駐させるより対話 shell が主目的になる。 そのため、起動時 user creation の単純さを優先できる。

sudo groupを安易に付けない

作業 user へ host と同じ UID/GID を与えても、container 内で sudo/admin 権限を常用させる必要はない。 root が必要な初期化は entrypoint で済ませ、対話 shell は通常 user に落とす。

必要な補助 group は用途ごとに限定する。

dialout:
  serial device access

docker:
  Docker socket access

containerd-related group:
  containerd socket access

socket を bind mount する場合は、host 側権限をそのまま container へ持ち込むことになる。 便利だが、container breakout 相当の権限になることを理解して使う。

対話shellのTTY問題

su - user -c fish のような入り方では、非標準の TTY や devcontainer 内で job control 周りの warning が出ることがある。 日常 CLI container では、login shell の再現性より、TTY と supplementary groups が正しく動くことを優先する。

entrypoint:
  gosu user command

interactive shell:
  setpriv --init-groups ...

gosu は default command を通常 user で実行する用途に合う。 一方で、後続の対話 shell では setpriv --init-groups のように group 初期化を明示できる形が扱いやすい。

コンテナは停止後に残さない

日常 CLI container は、実行中の常駐コンテナがあれば docker exec で再利用する。 ただし、停止済み container object は再利用しない。 shell.bashdocker run --rm で起動し、停止したら container object を自動削除する。 次回は host の現在状態に合わせて新しい container を作る。

この方針にすると、停止済みの /cli-tool-docker が Docker の名前を保持して次回の docker run --name cli-tool-docker と衝突する問題を避けられる。 また、--init、bind mount、Docker socket group、entrypoint の user/group 調整を変更した時に、古い停止済み container を誤って再利用しない。

消えるのは container 自身と container writable layer である。 host の $HOME は bind mount なので、日常作業で作ったファイルは container 削除の対象外になる。 一方で、停止後の docker logs cli-tool-dockerdocker inspect cli-tool-docker はできない。 停止後調査を残したい場合は、停止前に logs/inspect を保存するか、検証用には別名の docker run --rm なし container を使う。

旧設定で起動していた停止済み container が残っている場合は、docker ps では見えない。 確認には docker ps -a --filter name=cli-tool-docker を使う。 現在の shell.bash は、そのような停止済み container を見つけた場合、再利用せず docker rm してから新規作成する。

macOSのdocker credential helper

macOS host の Docker config を container 内へ mount すると、Linux container 側に存在しない credential helper が指定されていることがある。

典型例。

error getting credentials: executable file not found in PATH

原因は、host の Docker config が macOS 用 credential helper を指しているためである。 Linux container 内ではその helper が存在しない。

対処方針:

  • host の Docker config をそのまま使うか、container 専用 config を使うかを分ける
  • Linux container 用には Linux で利用できる credential helper を使う
  • registry 操作が不要なら Docker config を mount しない
  • credential helper の中身や secret は image に焼かない

Docker credential は平文 config だけの問題ではなく、host の keychain / password store / secret service との組み合わせで動く。 日常 CLI image では、credential 管理を image 内の永続状態にしない方がよい。

検証項目

起動後に最低限見るもの。

id
whoami
printf '%s\n' "$HOME"
touch /workspace/.container-write-test
ls -ln /workspace/.container-write-test
rm /workspace/.container-write-test

Docker socket を使うなら。

docker version
docker ps

serial device を使うなら。

id
ls -l /dev/ttyACM0

所有者、group、supplementary groups が期待通りであることを確認してから、日常作業に使う。

まとめ

日常 CLI container では、image に個人 UID/GID を焼くより、shell wrapper と entrypoint で runtime に合わせる方が扱いやすい。 重要なのは、root で container を起動して初期化し、対話作業は host user 相当へ落とし、補助 group と bind mount の権限を明示的に扱うことだ。