Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@
## [Unreleased]

### Added
- **VS Code Server をコンテナ再作成をまたいで保つ**ようにしました (PLAN36)。
`~/.vscode-server` はこれまでコンテナ層 (揮発) にあったため、`devbase up` で
コンテナを作り直すたびに VS Code の attach で **215MB の再ダウンロード**
(約 55 秒) と拡張機能の再インストールが走っていました。コンテナ 1 つにつき 1 本の
named volume `devbase_vscode_<project>_<index>` を `~/.vscode-server` へ
マウントし、本体・拡張機能・接続トークンをプロジェクトの寿命で保ちます。

共有せずコンテナ単位にするのは、VS Code Server が「1 マシン 1 セット」の状態
(`data/Machine/.connection-token-<commit>` など) を持つためです。名前に
プロジェクト名とインスタンス番号を含めるので、`scale > 1` の同時 attach でも
別プロジェクトの同時起動でも状態が混ざりません。

反映には**ベースイメージの再ビルド**が要ります (`devbase container build --no-cache`)。
空のボリュームは root 所有で作られるため、entrypoint が所有者を初期化します。
VS Code 本体の更新 (`commit` ハッシュの変更) 時は従来どおり取得が走ります。
ボリュームは `devbase down` でも残るので、使わなくなったプロジェクトの分は
[トラブルシューティング](docs/user/troubleshooting.md#vs-code-server-のボリュームが溜まっている)
の手順で削除してください。

- **Antigravity CLI (`agy`) をベースイメージへ追加**しました。Google の AI コーディング
エージェントを、既存の `claude` / `gemini` / `codex` / `kiro` と同じくコンテナ内から
すぐ使えます。エイリアス `agy` は確認プロンプトを省く
Expand Down
17 changes: 17 additions & 0 deletions containers/base/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,20 @@ devbase_setup_ai_settings() {
done
}

# ===================================================================
# PLAN36: VS Code Server の永続ディレクトリ
# ===================================================================
# ~/.vscode-server にはコンテナ 1 つにつき 1 本の named volume が割り当てられる
# (devbase_vscode_<project>_<index>)。空のボリュームは root 所有で作られるため、
# そのままでは開発ユーザーが VS Code Server をインストールできない。
#
# マウントが無い構成でもディレクトリを作るだけで済み、VS Code の動作は変わらない。
devbase_setup_vscode_server_dir() {
local home_root="$1" owner="${2:-${USERNAME:-ubuntu}}"

devbase_ensure_persistent_root "${home_root}/.vscode-server" "$owner"
}

# ===================================================================
# PLAN39: GCP の認証モードと gcloud / gws の設定ディレクトリ
# ===================================================================
Expand Down Expand Up @@ -528,6 +542,9 @@ USERNAME="${USERNAME:-ubuntu}"
devbase_setup_cloud_config_dirs "$USERNAME"
devbase_setup_gcp_credentials "/home/${USERNAME}"

# 1.5. VS Code Server の永続ディレクトリ (PLAN36)
devbase_setup_vscode_server_dir "/home/${USERNAME}" "$USERNAME"

# 2. Setup Git configuration
if [ -n "$GIT_USER_NAME" ]; then
git config --global user.name "$GIT_USER_NAME" 2>/dev/null || true
Expand Down
42 changes: 41 additions & 1 deletion docs/user/container-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,14 @@ graph LR

## ボリューム構造

devbase のコンテナは 3 種類のボリュームを使用します。
devbase のコンテナは 4 種類のボリュームを使用します。

| ボリューム名 | マウント先 | 共有範囲 | 用途 |
|-------------|-----------|---------|------|
| `devbase_home_ubuntu` | `/persistent/ai` | 全コンテナで共有 | 契約やテナントに紐づかない共通資産(`~/.claude/plugins` / `skills` / `commands` / `CLAUDE.md` / `settings.json`、`.codex` / `.serena` / `.kiro`、SSH 鍵、共有ファイル置き場 `share`)|
| `devbase_home_{group}` | `/persistent/group` | 同じアカウントグループのコンテナで共有 | 企業テナントに紐づくもの(Claude Code の認証と会話ログ、`.gemini`、gcloud / gws の設定ディレクトリ)|
| `devbase_work_{index}` | `/work` | 同じ index のコンテナで共有(プロジェクト間も共有) | プロジェクトのソースコード、作業ファイル |
| `devbase_vscode_{project}_{index}` | `/home/ubuntu/.vscode-server` | 共有しない(コンテナ 1 つに 1 本)| VS Code Server 本体・拡張機能・接続トークン |

> **Note:** `devbase_home_ubuntu` は **`/persistent/ai`** にマウントされます(`/home/ubuntu` への直接マウントは廃止)。`/home/ubuntu` 直下はコンテナ層(揮発)で、永続化されるのは entrypoint が `/persistent/ai` / `/persistent/group` 配下へ symlink する設定ファイルのみです。シェル履歴など symlink 対象外のファイルは再生成で失われます。

Expand Down Expand Up @@ -184,6 +185,26 @@ DEVBASE_ACCOUNT_GROUP=kkg

Google 認証の具体的な手順は [Google 認証ガイド](google-auth.md) を参照してください。

### VS Code Server の永続化

VS Code を attach すると、コンテナ内に VS Code Server(本体 `bin/<commit>`・拡張機能・
接続トークン)が入ります。ここは `~/.vscode-server` で、以前はコンテナ層(揮発)にあったため
`devbase up` でコンテナを作り直すたびに **215MB の再ダウンロード**(約 55 秒)が走っていました。

現在は `devbase_vscode_{project}_{index}` を `~/.vscode-server` にマウントするため、
コンテナを作り直しても本体と拡張機能が残ります。

このボリュームだけは**共有しません**。VS Code Server は「1 マシン 1 セット」の状態
(`data/Machine/.connection-token-<commit>` など)を持ち、複数のコンテナが同時に書くと
接続トークンを奪い合うためです。名前にプロジェクト名とインスタンス番号の両方を含めることで、
`scale > 1` の同時 attach でも、別プロジェクトの同時起動でも状態が混ざりません。

- 初回 attach と VS Code 本体のバージョン更新時(`commit` ハッシュが変わるとき)は
ダウンロードが走ります。減るのは**コンテナ再作成のたびの再取得**です
- プロジェクトが `compose.yml` で `~/.vscode-server` を自分でマウントしている場合、
devbase は上書きしません
- スナップショット(`devbase snapshot`)の対象外です。失っても attach し直せば再取得されます

### ボリュームの永続性

- ボリュームは `devbase down` でもコンテナが削除されても保持されます
Expand All @@ -202,6 +223,25 @@ docker volume ls | grep devbase
docker volume inspect devbase_home_ubuntu
```

#### 使わなくなった VS Code Server ボリュームを消す

`devbase_vscode_*` はプロジェクトを削除しても自動では消えません。attach したコンテナ 1 つ
あたり **約 1.6GB** を使うため、使わなくなったプロジェクトの分は手で削除します。

```bash
# 一覧(サイズ付き)
docker volume ls --filter name=devbase_vscode_ --format '{{.Name}}'
docker system df -v | grep devbase_vscode_

# 使用中のコンテナが無いことを確認してから削除する
docker ps -a --filter volume=devbase_vscode_<project>_1
docker volume rm devbase_vscode_<project>_1
```

削除しても失われるのは VS Code Server のキャッシュだけです。次の attach で再取得され、
設定(`~/.claude` などの AI 設定)には影響しません。稼働中のコンテナが掴んでいるボリュームは
`docker volume rm` が拒否するので、先に `devbase down` してください。

> **Warning:** `devbase_home_ubuntu` ボリューム(`/persistent/ai`、および symlink 経由でアクセスする `~/.claude/plugins` / `~/share` 等)は全プロジェクトで共有されます。ここにプロジェクト固有のファイルを置くと、他のプロジェクトにも影響します。プロジェクト固有のファイルは `/work` に配置してください。

## AI 設定の永続化
Expand Down
57 changes: 57 additions & 0 deletions docs/user/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,63 @@ docker volume ls | grep <project>
docker volume rm <volume_name>
```

### VS Code Server のボリュームが溜まっている

**症状:**

`devbase_vscode_*` のボリュームが増え、`docker system df` のボリューム使用量が大きい。
使わなくなったプロジェクトの分も残っている。

**原因:**

`~/.vscode-server`(VS Code Server 本体・拡張機能)はコンテナ 1 つにつき 1 本の
`devbase_vscode_{project}_{index}` へ永続化されます。attach したコンテナ 1 つあたり
**約 1.6GB** で、プロジェクトを削除してもボリュームは自動では消えません。

**解決策:**

```bash
# VS Code Server ボリュームの一覧
docker volume ls --filter name=devbase_vscode_ --format '{{.Name}}'

# サイズを確認
docker system df -v | grep devbase_vscode_

# 使用中のコンテナが無いことを確認してから削除する
docker ps -a --filter volume=devbase_vscode_<project>_1
docker volume rm devbase_vscode_<project>_1
```

失われるのは VS Code Server のキャッシュだけで、次に attach すると再取得されます
(その 1 回だけダウンロードが走ります)。AI 設定や作業ファイルには影響しません。

> **Note:** `docker volume prune` は稼働中でないコンテナが参照するボリュームも消します。
> 対象を絞りたい場合は上のように名前を指定して削除してください。

### VS Code の attach で毎回ダウンロードが走る

**症状:**

`devbase up` のあとに VS Code を attach すると、毎回 `Installing VS Code Server` が出て
215MB のダウンロードが走る。

**原因と解決策:**

1. **entrypoint が古い** — VS Code Server の永続化にはベースイメージの再ビルドが要ります。
`devbase container build --no-cache` を実行してから `devbase up` してください。
2. **VS Code 本体が更新された** — `bin/<commit>` は VS Code クライアントのビルドごとに
分かれます。クライアントを更新した直後の 1 回はダウンロードが走ります(仕様)。
3. **ボリュームがマウントされていない** — 次で確認できます。

```bash
docker inspect <project>-dev-1 \
--format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{"\n"}}{{end}}' \
| grep vscode
```

`devbase_vscode_<project>_1 -> /home/ubuntu/.vscode-server` が出ない場合は、
`.docker-compose.scale.yml` が古いままです。`devbase up` で再生成されます。

## 問題が解決しない場合

上記の方法で解決しない場合は、以下の情報を添えて [GitHub Issues](https://github.com/devbasex/devbase/issues) に報告してください。
Expand Down
54 changes: 52 additions & 2 deletions lib/devbase/volume/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@
from .manager import (
get_ai_volume_for_index,
get_group_volume,
get_vscode_volume_for,
get_work_volume_for_index,
resolve_account_group,
resolve_project_name,
)

logger = get_logger(__name__)

# 旧 /home/ubuntu マウントは非推奨のため scale 生成時に除去する
_DEPRECATED_TARGET = '/home/ubuntu'

# VS Code Server の状態ディレクトリ (PLAN36)。コンテナの書き込みレイヤに置くと
# devbase up のたびに 215MB の再ダウンロードが走るため、named volume を宛てる。
VSCODE_SERVER_TARGET = '/home/ubuntu/.vscode-server'


def get_dev_service_name() -> str:
"""Get development service name from environment variable or default to 'dev'"""
Expand Down Expand Up @@ -70,15 +76,28 @@ def _volume_target(vol: Any) -> Optional[str]:
return None


def _declares_target(service: Mapping[str, Any], target: str) -> bool:
"""サービスが ``target`` へのマウントを自分で書いているか。"""
return any(
_volume_target(vol) == target
for vol in (service.get('volumes') or [])
)


def _replace_volumes_for_instance(
volumes: list, ai_volume: str, work_volume: str, group_volume: str,
vscode_volume: Optional[str] = None,
) -> list:
"""Replace volume mounts in a service's volumes list for a specific instance.

/home/ubuntu mounts are skipped (deprecated).
/persistent/ai is mapped to ai_volume (shared by every container).
/persistent/group is mapped to group_volume (shared within the account group).
/work is mapped to work_volume.

``vscode_volume`` は ``~/.vscode-server`` へ足すボリューム (PLAN36)。
上の 3 つと違って**差し替えではなく追加**で、プロジェクトが同じマウント先を
書いている場合は呼び出し側が ``None`` を渡して手を出さない。
"""
replacements = {
'/persistent/ai': ai_volume,
Expand Down Expand Up @@ -114,10 +133,15 @@ def _replace_volumes_for_instance(
for target, source in replacements.items()
if target not in replaced_targets
)
if vscode_volume:
new_volumes.append(f"{vscode_volume}:{VSCODE_SERVER_TARGET}")
return new_volumes


def _build_volumes_section(config: dict, scale: int, group_volume: str) -> dict:
def _build_volumes_section(
config: dict, scale: int, group_volume: str,
vscode_volumes: Sequence[str] = (),
) -> dict:
"""Build the volumes section for a scaled compose file."""
# Copy original volumes (mysql, valkey, etc.) from config
volumes: Dict[str, Any] = {
Expand All @@ -136,6 +160,11 @@ def _build_volumes_section(config: dict, scale: int, group_volume: str) -> dict:
for i in range(1, scale + 1):
volumes[get_work_volume_for_index(i)] = {'external': True}

# Add VS Code Server volumes (PLAN36). 実際にマウントする分だけ宣言する。
# external は実体の存在を要求するため、使わない名前を書くと起動が落ちる。
for name in vscode_volumes:
volumes[name] = {'external': True}

return volumes


Expand Down Expand Up @@ -355,6 +384,7 @@ def _build_dev_instance(
group_volume: str,
secret_env_names: Sequence[str] = (),
dev_environment: Optional[Mapping[str, str]] = None,
vscode_volume: Optional[str] = None,
) -> dict:
"""Build the service definition for one scaled dev instance (dev-<index>)."""
service = copy.deepcopy(dev_service)
Expand All @@ -374,6 +404,7 @@ def _build_dev_instance(
work_volume = get_work_volume_for_index(index)
service['volumes'] = _replace_volumes_for_instance(
service.get('volumes', []), ai_volume, work_volume, group_volume,
vscode_volume,
)

return service
Expand All @@ -385,6 +416,7 @@ def _build_scaled_services(
secret_names: Optional[_SecretNames] = None,
secret_services: Optional[Mapping[str, Set[str]]] = None,
dev_environment: Optional[Mapping[str, str]] = None,
vscode_volumes: Sequence[str] = (),
) -> dict:
"""Build the services section: non-dev services + dev-1..dev-N instances.

Expand All @@ -394,6 +426,9 @@ def _build_scaled_services(
scaled_services = {}
secret_names = secret_names if secret_names is not None else _SecretNames()
receivers = dict(secret_services or {})
# インスタンス番号 → VS Code Server ボリューム。マウントしない構成 (空列) では
# 引けず None になる。
vscode_by_index = dict(enumerate(vscode_volumes, start=1))

# Copy non-dev services (mysql, valkey, etc.) — rewriting any
# `depends_on: <dev>` reference to the scaled instances (dev-1..N) so
Expand Down Expand Up @@ -428,6 +463,7 @@ def _build_scaled_services(
dev_service, dev_service_name, i, group_volume,
secret_names.for_dev,
dev_environment=dev_environment,
vscode_volume=vscode_by_index.get(i),
)
return scaled_services

Expand Down Expand Up @@ -602,11 +638,24 @@ def generate_scaled_compose(
secret_env_names, global_env_names, project_env_names,
dev_excluded=gcp_auth.key_only_env_names(auth_mode))

# VS Code Server は再作成をまたいで保つためコンテナ 1 つに 1 本の named
# volume を宛てる (PLAN36)。プロジェクトが自分で ~/.vscode-server を
# マウントしている場合は、その指定を奪わないよう devbase 側は何もしない。
if _declares_target(dev_service, VSCODE_SERVER_TARGET):
vscode_volumes: List[str] = []
else:
project_name = resolve_project_name()
vscode_volumes = [
get_vscode_volume_for(project_name, i)
for i in range(1, scale + 1)
]

scaled_services = _build_scaled_services(
services, dev_service, dev_service_name, scale, group_volume,
secret_names=secret_names,
secret_services=secret_services,
dev_environment=dev_environment,
vscode_volumes=vscode_volumes,
)

# 列挙を絞るだけでは、元の compose.yml が environment に**直書き**している
Expand All @@ -624,7 +673,8 @@ def generate_scaled_compose(

scaled_config = {
'services': scaled_services,
'volumes': _build_volumes_section(config, scale, group_volume),
'volumes': _build_volumes_section(
config, scale, group_volume, vscode_volumes),
'networks': _build_networks_section(config),
}

Expand Down
Loading
Loading