Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1633d26
docs(PLAN52): 実装計画を追加
takemi-ohama Sep 13, 2026
b6ea279
feat(project): project.local.yml の docker 節を読む (PLAN52 Task 1)
takemi-ohama Sep 13, 2026
1ad3cae
feat(utils): docker context の解決・確定・反映と gid 取得 (PLAN52 Task 2)
takemi-ohama Sep 13, 2026
5f0c4e3
feat(container): lifecycle コマンドに docker context を通す (PLAN52 Task 3)
takemi-ohama Sep 13, 2026
63de4a3
feat(volume): リモート扱いの生成で bind mount の ~ を docker.home で展開する (PLAN52 T…
takemi-ohama Sep 13, 2026
3a2dfa3
feat(build): shell の build が --context を env exec へ引数で渡し、docker 呼び出しを…
takemi-ohama Sep 13, 2026
6d17565
fix(build): --context を抜いた残りを _DEVBASE_ARGS へ戻し、既存の dispatch 行を保つ
takemi-ohama Sep 13, 2026
9a61d63
feat(editor): 解決した docker context を attach URI の settings.context に載せ…
takemi-ohama Sep 13, 2026
8226b3b
docs: project.local.yml とリモート Docker の使い方を書く (PLAN52 Task 7)
takemi-ohama Sep 13, 2026
3339048
Test: characterize remote Docker context branches and boundaries
takemi-ohama Sep 13, 2026
144399a
Test: characterize remote Docker context runner exceptions and open-i…
takemi-ohama Sep 13, 2026
8c99a22
Refactor: extract_method — 構造改善ラウンド 2 (R2-001..R2-004)
takemi-ohama Sep 13, 2026
9d37270
Refactor: consolidate_duplication — lib/devbase/commands/container.py…
takemi-ohama Sep 13, 2026
ff50d5b
Refactor: extract compose phases and consolidate project builds
takemi-ohama Sep 13, 2026
6c541e7
Revert "Refactor: extract compose phases and consolidate project builds"
takemi-ohama Sep 13, 2026
9b9f065
Refactor: consolidate_duplication — lib/devbase/volume/compose.py#_ma…
takemi-ohama Sep 13, 2026
bd57f6c
refactor(container): 接続先の確定で project.local.yml を二度読まない
takemi-ohama Sep 13, 2026
739faa3
fix: レビュー指摘を反映 (現在の context の取得を docker_context に一本化・build --context …
takemi-ohama Sep 13, 2026
8d710aa
fix: レビュー指摘を反映 (エディタの推測は docker が実際に使う context に合わせる・長い書式 bind の相対 so…
takemi-ohama Sep 13, 2026
184a012
fix: レビュー指摘を反映 (切替元の機密を切替先 env の読み込み前に落とす・env exec の project.local.ym…
takemi-ohama Sep 13, 2026
0abb782
test: container context のテストで os.environ をテストごとに戻す
takemi-ohama Sep 13, 2026
baad328
test: up_harness で context 関連の env とモジュール状態を空にしてから始める
takemi-ohama Sep 13, 2026
badeef6
docs(spec): 別ホストの Docker への dev コンテナ起動の確定仕様を残し、PLAN52 を issues/old へ移す
takemi-ohama Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@

## [Unreleased]

### Added

- **別ホストの Docker に dev コンテナを立てられるようにしました(PLAN52 / #162)。**
`projects/<name>/project.local.yml`(gitignore 対象の個人・機材ごとの設定)に
`docker.context` / `docker.home` / `docker.gid` を書くと、`devbase up/down/ps/logs/login/
scale/build/rebuild` がその docker context の daemon を相手に動きます。優先順位は
CLI `--context` > env `DEVBASE_DOCKER_CONTEXT` > `project.local.yml` > 現在の context です。
- リモート扱い(解決した context が現在の context と異なる)では `DOCKER_GID` をリモート側で
取得して `.cache/docker-gid/<context>` に控え、bind mount の `~` を `docker.home` で展開し、
自動スナップショットを飛ばします
- `devbase up` が開く VS Code の attach URI に `settings.context` を付け、ローカル端末からも
リモートのコンテナへ attach できます。Remote-SSH 統合端末では手元で直接 attach する
フラット URI も表示します
- `devbase env exec --context NAME` を追加し、shell の `devbase build` はそこを通して docker を
呼びます
- 詳細は `docs/user/environment-variables.md` の「リモート Docker」と
`docs/user/project-yml.md` の「`project.local.yml`」

### Changed

- `project.yml` に `docker:` を書くと、`project.local.yml` へ移すよう案内するエラーになります
- docker context を解決したときは、docker が `DOCKER_CONTEXT` より優先する `DOCKER_HOST` を
警告して子プロセスから外します(設定が無いときは従来どおり)

## [3.2.2] - 2026-09-04

base イメージで AI CLI の alias 設定を一般ユーザーが読み込めない問題を修正しました。
Expand Down
46 changes: 42 additions & 4 deletions bin/devbase
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,21 @@ export DEVBASE_ROOT
# Docker Compose の変数展開が機密を必要とする場合があるため、compose の呼び出しは
# Python 経由で機密を注入して実行する (plan35 §4.4 / §11.2)。復号結果は子プロセスの
# 環境変数としてだけ渡り、ファイルには書き出されない。
#
# docker context (PLAN52) も同じ経路で届く。`env exec` がカレントプロジェクトの
# project.local.yml と env から context を解決して DOCKER_CONTEXT を載せるため、
# shell から docker を叩く箇所 (buildx build / image inspect / compose build) は
# すべてここを通す。`devbase build --context NAME` の値は _BUILD_CONTEXT に保持し、
# 環境変数ではなく引数で渡す。環境変数に写すと、Python 側が dispatch の前に注入する
# .env の同名キー (DEVBASE_DOCKER_CONTEXT) に上書きされる。
#
# `--${_BUILD_CONTEXT:+...}` は、_BUILD_CONTEXT があれば `--context NAME -- "$@"`、
# 無ければ `-- "$@"` に展開される (`--` の直後に context の語を続けるかどうか)。
_BUILD_CONTEXT=""
compose_with_secrets() {
ensure_uv
PYTHONPATH="${DEVBASE_ROOT}/lib:$PYTHONPATH" \
uv run --project "$DEVBASE_ROOT" python -m devbase.cli env exec -- "$@"
uv run --project "$DEVBASE_ROOT" python -m devbase.cli env exec --${_BUILD_CONTEXT:+context "$_BUILD_CONTEXT" --} "$@"
}

cmd_build() {
Expand Down Expand Up @@ -120,7 +131,7 @@ cmd_build() {
fi

echo "Building ${base_image}:latest..."
if docker buildx build --load -t "${base_image}:latest" "$container_dir" "$@"; then
if compose_with_secrets docker buildx build --load -t "${base_image}:latest" "$container_dir" "$@"; then
echo "✓ ${base_image} built successfully"
return 0
else
Expand Down Expand Up @@ -211,7 +222,7 @@ cmd_build() {
fi
else
# Fallback: check if devbase-base exists
if ! docker image inspect devbase-base:latest >/dev/null 2>&1; then
if ! compose_with_secrets docker image inspect devbase-base:latest >/dev/null 2>&1; then
echo ""
echo "[1/2] Building devbase-base..."
if ! build_base_image "devbase-base" "$@"; then
Expand Down Expand Up @@ -432,6 +443,33 @@ case "$_resolved_cmd" in
# - --expires: イメージ作成日の判定が必要で、shell では RFC3339 日付パースが
# 非可搬なため (build --expires=N / rebuild / up が共通の期限リゾルバを使う)。
build)
# `--context NAME` / `--context=NAME` は最初に抜き取る (PLAN52)。下の走査より
# 後に置くと NAME が単体イメージ名として拾われ、Python の単体ビルドへ誤分岐する。
# 値は _BUILD_CONTEXT に保持し、compose_with_secrets (env exec) と Python の
# project build へ引数で渡す。
_build_args=()
_expect_context=0
_context_given=0
for _ba in "${_DEVBASE_ARGS[@]}"; do
if [ "$_expect_context" = 1 ]; then
_BUILD_CONTEXT="$_ba"; _expect_context=0; continue
fi
case "$_ba" in
--context) _expect_context=1; _context_given=1 ;;
--context=*) _BUILD_CONTEXT="${_ba#--context=}"; _context_given=1 ;;
*) _build_args+=("$_ba") ;;
esac
done
# 値なし・空・空白のみは Python 側 (cli.py の _non_empty) と同じく exit 2 で止める。
# 空のまま通すと ${_BUILD_CONTEXT:+...} が展開されず、手元の daemon でビルドが走る。
_BUILD_CONTEXT="${_BUILD_CONTEXT#"${_BUILD_CONTEXT%%[![:space:]]*}"}"
_BUILD_CONTEXT="${_BUILD_CONTEXT%"${_BUILD_CONTEXT##*[![:space:]]}"}"
if [ "$_expect_context" = 1 ] || { [ "$_context_given" = 1 ] && [ -z "$_BUILD_CONTEXT" ]; }; then
echo "Error: --context requires a non-empty context name" >&2; exit 2
fi
# bash 3.2 は空配列の "${arr[@]}" を set -u で未定義扱いするが、この wrapper は
# set -u を使わないので、抜き取った残りをそのまま _DEVBASE_ARGS へ戻してよい。
Comment thread
takemi-ohama marked this conversation as resolved.
_DEVBASE_ARGS=(${_build_args[@]+"${_build_args[@]}"})
_has_expires=0
_build_image=""
for _ba in "${_DEVBASE_ARGS[@]}"; do
Expand All @@ -442,7 +480,7 @@ case "$_resolved_cmd" in
esac
done
if [ "$_has_expires" = 1 ] || [ -n "$_build_image" ]; then
run_python project build "${_DEVBASE_ARGS[@]}"
run_python project build "${_DEVBASE_ARGS[@]}" ${_BUILD_CONTEXT:+--context "$_BUILD_CONTEXT"}
else
cmd_build "${_DEVBASE_ARGS[@]}"
fi
Expand Down
211 changes: 211 additions & 0 deletions docs/specifications/remote-docker-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# 別ホストの Docker への dev コンテナ起動(docker context)

## 概要

devbase は、プロジェクトごとの個人設定 `projects/<name>/project.local.yml` に docker context の
名前を書くと、そのプロジェクトの `up` / `down` / `ps` / `logs` / `login` / `scale` / `build` /
`rebuild` を別ホストの docker daemon へ向ける。compose クライアントと機密の復号は手元で行い、
daemon だけがリモートにある。リモートに要るのは docker CLI・dockerd・sshd で、devbase・
`projects/`・機密鍵をリモートへ複製しない。`devbase up` が開く VS Code は、attach URI の
`settings.context` でそのホストのコンテナへ接続する。

## 用語

| 用語 | 意味 |
| --- | --- |
| docker context | docker CLI が daemon への接続先を名前で切り替える仕組み(`docker context ls` の名前) |
| 現在の context | `DOCKER_CONTEXT` と `DOCKER_HOST` を外した環境で `docker context show` が返す名前 |
| 解決した context | 優先順位に従って devbase が決めた context 名。未指定なら `None`(従来どおり CLI に委ねる) |
| リモート扱い | 解決した context が `None` でなく、現在の context と異なる(または現在の context を取得できない)状態 |
| ローカル扱い | 上記以外。従来と同じ振る舞い |

## 構成要素

| 要素 | 置き場所 | 責務 |
| --- | --- | --- |
| 個人設定の読み込み | `lib/devbase/project/local_config.py` | `project.local.yml` を読み、`docker` 節を検証して `DockerSettings` にする |
| context の解決・確定・反映 | `lib/devbase/utils/docker_context.py` | `choose_context` / `resolve_target` / `apply` / `reapply` / `reset` / `ensure_remote_gid` / `current_context` / `effective_context` |
| lifecycle コマンド | `lib/devbase/commands/container.py` | `--context` の受け取り、操作の前後の `reset`、`up` / `scale` での接続先の確定と gid |
| shell の `build` | `bin/devbase` | `--context` の抜き取りと `env exec --context` 経由の docker 呼び出し |
| `env exec` | `lib/devbase/commands/env.py` | 子プロセスの環境へ `DOCKER_CONTEXT` を載せる |
| bind mount の書き換え | `lib/devbase/volume/bind_mounts.py`、`compose.py` | 生成物の `~` を `docker.home` で展開し、書き換えられない mount を警告する |
| attach URI | `lib/devbase/editor/opener.py` | `settings.context` の決定とフラット URI の提示 |

## 仕様

### context の解決

優先順位は **CLI `--context` > 環境変数 `DEVBASE_DOCKER_CONTEXT` > `project.local.yml` の
`docker.context` > 未指定** である。環境変数の空文字(空白のみを含む)は未指定として扱う。
この段階では docker を呼ばない。

`--context` は `project` / `container` 配下の `up` / `down` / `ps` / `logs` / `login` / `scale` /
`build` / `rebuild`、トップレベルのショートカット `up` / `down` / `ps` / `login` / `scale` /
`build` / `rebuild`、および `env exec` が受け付ける。空文字と空白のみは終了コード 2 で拒む
(Python の parser と `bin/devbase` の両方)。

### リモート扱いの判定

`up` と `scale` は、解決した context を現在の context と比べる。現在の context の問い合わせは
`DOCKER_CONTEXT` と `DOCKER_HOST` を取り除いた環境で行う。どちらかが残ると docker は
それぞれ設定先自身・`default` を返し、判定が常に一方へ倒れるためである。

| 解決した context | 現在の context との関係 | 扱い |
| --- | --- | --- |
| `None` | 問い合わせない | ローカル |
| 非 `None` | 同じ | ローカル |
| 非 `None` | 異なる、または取得できない | リモート |

`docker.home` / `docker.gid` はリモート扱いのときだけ使う。CLI / 環境変数で
`project.local.yml` の `docker.context` と**別の名前**へ向けたときは、ファイルの `home` / `gid`
を使わず警告する(別の機材の値を持ち込まない)。

### 環境への反映

解決した context は環境変数 `DOCKER_CONTEXT` として `os.environ` へ載せ、以降の `docker` /
`docker compose`・`pre-up` / `deploy` フック・`up` からの自動ビルドがすべて継承する。
反映は次の条件を保つ。

- context が `None` なら環境を一切触らない
- `DOCKER_HOST` があれば警告して取り除く。docker は `DOCKER_HOST` を `DOCKER_CONTEXT` より
優先するため、残すと context が効かない
- リモート扱いの `up` / `scale` では `DOCKER_GID` をリモート側の gid に置き換える
- 反映は冪等で、機密の注入(`_inject_secrets`)の直後に再適用する。機密ストアに
`DOCKER_CONTEXT` / `DOCKER_GID` / `DOCKER_HOST` があっても確定した接続先が残る
- 控えは lifecycle 操作の単位で生き、`_dispatch_lifecycle` が開始時と終了時に `reset` して
3 変数を元の値へ戻す。1 プロセスで操作を続ける TUI で、前の操作の接続先を持ち越さない

`name` でプロジェクトを切り替える経路(`project down B` 等)は、切替元の機密を落として
から切替先の `env` を読み、切替先の機密を注入した後に context を解決する。

### リモート側の gid

`group_add: ["${DOCKER_GID}"]` に渡す gid は、`docker.gid` の明示 → 控え
`$DEVBASE_ROOT/.cache/docker-gid/<context>` → `DOCKER_CONTEXT` 付きの
`docker run --rm -v /var/run/docker.sock:/s alpine:3 stat -c %g /s` の順で決める。取得した値は
控えに書く。取得に失敗した(docker が非ゼロ・出力が整数でない)ときは、docker のエラーと
`docker.gid` の書き方を示して `up` を非ゼロで終える。取得した値が `0` のときは、socket が
root 所有か rootless Docker の可能性を警告して続行する。控えは自動では消さない。

### bind mount の `~`

リモート扱いの構成生成では、生成物 `.docker-compose.scale.yml` の全サービスの bind mount で
`~` と `~/...` を `docker.home` に置き換える。短い書式・長い書式(`type: bind`)の両方に効く。
`~user/...` と相対パス(`/` でも `~` でも始まらない source)は書き換えず、一覧で警告する。
`docker.home` が無いリモート扱いでは、`~` 系と相対パスの mount を一覧で警告し、`docker.home`
の指定を促す。ローカル扱いでは書き換えない。

### 自動スナップショット

リモート扱いの `up` は自動スナップショットを作らず、警告を 1 行出す。`devbase snapshot` 系の
コマンドと `down` のローテーションは context を解決せず、従来どおり手元を対象にする。

### shell の `build` と `env exec`

`bin/devbase` の `build)` 分岐は、単体イメージ名の走査より前に `--context NAME` /
`--context=NAME` を抜き取り、シェル変数に保持する。値は環境変数へ写さず、
`compose_with_secrets`(`devbase env exec --context NAME -- ...`)と Python の
`project build --context NAME` へ引数で渡す。`cmd_build` の `docker buildx build` と
`docker image inspect` も `compose_with_secrets` を通す。

`env exec` はプロジェクト直下(`current_project_name` が決める `projects/<name>`)の
`project.local.yml` と環境変数、`--context` から context を解決し、機密を載せた**後**の辞書へ
`DOCKER_CONTEXT` を載せる。`up` からの自動ビルド(`_run_build`)は解決済みの context を
`bin/devbase build --context <name>` として引数で渡す。

### VS Code の attach URI

`settings.context` は「`DEVBASE_EDITOR_DOCKER_CONTEXT` の明示 → devbase が解決した context →
(ssh 先のときだけ)docker が実際に使う context(環境変数を外さない `docker context show`)」の
順で決める。解決した context があればローカル端末でもフラット URI に付ける。Remote-SSH 統合
端末でネスト URI と `settings.context` の両方が付くときは、手元の VS Code に同名の context が
あれば直接 attach できるフラット URI を info で提示する。

```mermaid
sequenceDiagram
participant U as up
participant R as 解決と確定
participant D as docker
participant C as 構成生成
U->>R: 解決(project_dir, --context, environ)
R->>D: docker context show(DOCKER_CONTEXT / DOCKER_HOST 抜き)
D-->>R: 現在の context
R-->>U: DockerTarget
U->>U: DOCKER_CONTEXT を載せる(DOCKER_HOST は外す)
alt リモート扱い
R->>D: docker run alpine stat(控えが無いとき)
D-->>R: gid
U->>U: DOCKER_GID を載せる / 自動スナップショットを飛ばす
end
U->>C: 生成(scale, secrets, docker_home, remote)
Note over U,C: 機密注入の直後に反映を再適用
U->>D: compose down / up / exec
U->>U: エディタ(docker_context)
```

## データ・設定

### `projects/<name>/project.local.yml`

git 管理しない。最上位に書けるのは `docker` だけで、他のキーは `ConfigError` になる。
`project.yml` に `docker:` を書くと、このファイルへ移す案内付きの `ConfigError` になる。
空ファイルは無いときと同じに扱う。

| キー | 型 | 検証 |
| --- | --- | --- |
| `docker.context` | 文字列 | 空・空白・制御文字を含むものは拒む |
| `docker.home` | 文字列 | `/` で始まる絶対パスのみ |
| `docker.gid` | 整数 | 0 以上。真偽値・文字列は拒む |

### `$DEVBASE_ROOT/.cache/docker-gid/<context>`

10 進の gid を 1 行で持つ。リモート扱いで gid を取得したときに書き、次回はこれを読む。
整数として読めない内容は無視して取り直す。

### 環境変数

| 名前 | 向き | 意味 |
| --- | --- | --- |
| `DEVBASE_DOCKER_CONTEXT` | 入力 | context の上書き(グローバル `.env` / プロジェクト `env` / shell)。空文字は未指定 |
| `DOCKER_CONTEXT` | 出力 | 解決した context。docker CLI と compose が読む |
| `DOCKER_GID` | 出力 | リモート扱いの `up` / `scale` でだけ上書き |
| `DOCKER_HOST` | 入力 | context を解決したときは警告して取り除く |
| `DEVBASE_EDITOR_DOCKER_CONTEXT` | 入力 | attach に使う context を手で決めたいときだけ。解決した context より優先 |

## セキュリティ

`project.local.yml` は接続先の実体(ホスト名・鍵・トークン)を持たず、context の名前だけを
持つ。接続先の実体は各マシンの `docker context create` が持つ。age の鍵・`.env`・
`project.local.yml` は手元に留まるが、復号済みの機密の値は従来のローカル構成と同じく
compose の変数展開を通じて接続先の daemon とコンテナへ渡る。接続先は機密を預けてよい
ホストに限る。

## 運用

- 設定が無ければ挙動は変わらない。`project.local.yml` を消せば元に戻る
- `docker context use` で現在の context 自体をリモートへ向けた状態は補正の対象外
- `devbase status` は手元の daemon だけを見る
- rootless Docker や socket が `root:root` の構成では `docker.gid` を明示する
- リモート側の gid が変わったら `.cache/docker-gid/<context>` を消すか `docker.gid` を書く
- イメージはホストごとに別物で、リモート側に無ければリモートでビルドされる

## テスト観点

- 設定の読み込みと検証(`tests/project/test_local_config.py`)、`project.yml` の `docker:` の拒否
- 優先順位・リモート判定・反映・再適用・reset・gid 取得(`tests/utils/test_docker_context.py`)
- `up` / `scale` / `down` / `ps` / `logs` / `login` の子プロセスに届く `DOCKER_CONTEXT` /
`DOCKER_GID` / `DOCKER_HOST`、自動スナップショットの回避、TUI とプロジェクト切替での漏れ、
機密注入後の維持、`--context` を受け付ける parser(`tests/commands/test_container_context.py`)
- `env exec` の `--context` と機密ストアより優先すること、サブディレクトリからの実行
(`tests/commands/test_env_exec_context.py`)
- shell の `build` が `--context` を抜き取り引数で渡すこと、空値の拒否、docker 直接呼び出しの
不在(`tests/cli/test_wrapper_build_context.py`)
- bind mount の展開と警告(`tests/volume/test_bind_mounts.py`、`test_compose_remote_home.py`)
- attach URI の `settings.context` とフラット URI の提示(`tests/editor/test_opener.py`)
- 実 daemon への接続(存在しない context のエラー、リモートでのビルドと attach)は手動確認

## 関連リンク

- [project.yml リファレンス](../user/project-yml.md)
- [環境変数ガイド「リモート Docker」](../user/environment-variables.md)
- [CLI リファレンス: project](../user/cli-reference/02-project.md)
Loading
Loading