Gitリポジトリのサブディレクトリ以下のみをDevContainer化した際に、Gitも使いたい

Gitリポジトリのサブディレクトリ以下のみをDevContainer化した際に、Gitも使いたい

2026.03.12

モノレポの特定サブディレクトリだけを DevContainer のワークスペースとして開き、かつコンテナ内で git diffgit commit などのGit操作をフルに使えるようにしたので紹介します。ポイントは 4つのマウントsparse-checkout の組み合わせです。

背景:なぜこれが必要か

私たちのリポジトリはモノレポ構成で、複数のサブプロジェクトが同居しています。

<repo-root>/
├── .git/
├── aws/            ← 今回の開発対象
│   └── .devcontainer/
│       └── devcontainer.json
├── other-project/
└── docs/

DevContainer で開発したいのは aws/ ディレクトリ以下だけです。VS Code の「フォルダを開く」で aws/ を指定すれば、ワークスペースは aws/ に限定され、その後 DevContainerを(適切な設定で(後述)、)Build・Openすれば aws/ ディレクトリ以下だけを参照できるDevContainerが出来上がります。

しかし、ここで問題が発生します。

問題:.git がワークスペースの外にある

.git/ はリポジトリルートにあるため、コンテナ内には存在しません。結果として各種Gitコマンド (git statusgit diffgit commit など) が一切使えなくなります。

$ git status
fatal: not a git repository (or any of the parent directories): .git

素朴な解決策とその限界

案1:リポジトリルートをワークスペースにする

全ファイルがマウントされるため動作しますが、不要なファイルが大量に見え、ワークスペースが煩雑になります。

また、現在のプロジェクトでは DevContainer を Claude Code を用いた開発におけるリスク緩和策 として活用しています。コンテナによる隔離環境で開発することで、例えばマルウェアを含む外部パッケージをインストールしてしまった場合でも、情報漏洩などの影響をコンテナ内に封じ込めることができます。

この観点で言うと、リポジトリルートをワークスペースにするのは好ましくありません。aws/ ディレクトリ以下のみで開発業務は遂行できるので、他のディレクトリをDevContainer内に含めてしまうのは不要なリスクです。

案2:GIT_DIR / GIT_WORK_TREE 環境変数を設定する

一見動作しますが、コンテナ内で起動される子プロセスにも伝播してしまいます。例えばTerraform がremote moduleをダウンロードする際に内部的に git を使うのですが、これらの環境変数が干渉してモジュールのダウンロードが失敗するケースがありました。

解決策:4つのマウント + sparse-checkout

最終的に落ち着いた構成は以下の通りです。

全体像

ホスト側                               コンテナ側
─────────────────                      ──────────────────
<repo>/aws/          ──bind──────────→ /workspace/aws        (workspaceMount)
<repo>/.git/         ──bind──────────→ /workspace/.git       (Git本体)
<repo>/.git/info/    ──bind(readonly)→ /git-info-host        (excludeコピー元)
Docker volume        ──volume────────→ /workspace/.git/info  (sparse-checkout書込用)

Git はワークスペース /workspace/aws から親ディレクトリを辿り、/workspace/.git を発見します。これは Git の標準的なリポジトリ検出メカニズムそのものです。

devcontainer.json の実装

{
  // ワークスペースを /workspace/aws にマウント
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace/aws,type=bind,consistency=delegated",
  "workspaceFolder": "/workspace/aws",

  "mounts": [
    // ① .git ディレクトリを /workspace/.git にバインドマウント
    "source=${localWorkspaceFolder}/../.git,target=/workspace/.git,type=bind",

    // ② ホストの .git/info をリードオンリーでマウント(exclude ファイルのコピー元)
    "source=${localWorkspaceFolder}/../.git/info,target=/git-info-host,type=bind,readonly",

    // ③ .git/info をボリュームで上書き(コンテナ側で sparse-checkout を書き込むため)
    "source=git-info-${devcontainerId},target=/workspace/.git/info,type=volume"
  ],

  "remoteEnv": {
    // 異なるマウントポイント間でのリポジトリ検出を許可
    "GIT_DISCOVERY_ACROSS_FILESYSTEM": "1",
    // sparse-checkout を有効化
    "GIT_CONFIG_COUNT": "1",
    "GIT_CONFIG_KEY_0": "core.sparseCheckout",
    "GIT_CONFIG_VALUE_0": "true"
  },

  "postCreateCommand": {
    "git-sparse-checkout": "${containerWorkspaceFolder}/.devcontainer/scripts/init-git-sparse-checkout.sh"
  }
}

初期化スクリプト

${containerWorkspaceFolder}/.devcontainer/scripts/init-git-sparse-checkout.sh
#!/bin/bash

# バインドマウントはホストの所有者情報を引き継ぐため、
# コンテナ内の vscode ユーザーが操作できるよう safe.directory に登録
git config --global --add safe.directory /workspace

# ボリュームが .git/info を覆い隠すため、ホストの exclude をコピー
if [ ! -f /workspace/.git/info/exclude ]; then
  sudo cp /git-info-host/exclude /workspace/.git/info/ 2>/dev/null \
    || sudo touch /workspace/.git/info/exclude
fi

# sparse-checkout 定義(aws/ のみを追跡対象に)
if [ ! -f /workspace/.git/info/sparse-checkout ]; then
  echo aws/ | sudo tee /workspace/.git/info/sparse-checkout
fi

# sparse-checkout ルールをインデックスに反映
git -C /workspace read-tree -mu HEAD

各要素の詳細解説

なぜ workspaceMount と workspaceFolder をカスタマイズするのか

.devcontainer/aws/ 内にあっても、workspaceMountworkspaceFolder を未設定にすると DevContainer はリポジトリルート全体をマウントします。つまり other-project/docs/ などもコンテナ内に見えてしまいます。

If git is present on the host's PATH and the folder containing .devcontainer/devcontainer.json is within a git repository, the current workspace mounted will be the root of the repository.

引用元: Change the default source code mount

workspaceMountworkspaceFolder を明示的に指定することで、以下の 3 点を制御しています。

  1. マウント対象を aws/ のみに限定source=${localWorkspaceFolder} により、aws/ だけがコンテナにマウントされます。他のサブディレクトリはコンテナ内に存在しません。
  2. consistency=delegated の指定 — デフォルトの cached に対し、delegated はホストへの書き戻しを遅延させる分、コンテナ内のファイル I/O パフォーマンスが向上します。
  3. マウント先パスの指定 — マウント先を /workspace/aws にすることで、親の /workspace/.git を配置する構造を作っています。(.git の配置設定はここではなく、mountsの①です)
    /workspace/          ← 通常のディレクトリ(マウントではない)
    ├── .git/            ← ホストの .git をバインドマウント
    └── aws/             ← ホストの aws/ をバインドマウント(ワークスペース)
    
    Git は /workspace/aws から親を辿って /workspace/.git を発見し、/workspace をワーキングツリーのルートとして認識します。

なぜ GIT_DISCOVERY_ACROSS_FILESYSTEM が必要か

/workspace/aws(bind mount)と /workspace/.git(別の bind mount)は異なるファイルシステム上にあります。Git はデフォルトでファイルシステム境界を越えた検索を行わないため、この環境変数がないとリポジトリの検出に失敗します。

なぜ .git/info にボリュームを重ねるのか

これがこの構成で最も巧妙なポイントです。

.git 全体はホストとバインドマウントで共有されています。ホスト側でも同じ .git を使っているため、コンテナ内で sparse-checkout ファイルを書き込むとホスト側にも影響してしまいます。

しかし、sparse-checkout はコンテナ固有の設定です(ホスト側では全ファイルが見えるべき)。

そこで、.git/info/ ディレクトリだけを Docker ボリュームで上書きします。

/workspace/.git/           ← バインドマウント(ホストと共有)
├── HEAD, refs/, objects/  ← ホストと同期
└── info/                  ← ボリュームで上書き(コンテナ固有)
    ├── exclude            ← ホストからコピー
    └── sparse-checkout    ← コンテナだけで使う設定

ただし、ボリュームが info/ を覆い隠すため、ホスト側にあった exclude ファイルも見えなくなります。これを解決するために、ホストの .git/info をリードオンリーで別パス(/git-info-host)にもマウントし、初回起動時にボリューム側へコピーしています。

なぜ sparse-checkout が必要か

コンテナ内には aws/ サブディレクトリしかマウントされていません。sparse-checkout を設定しないと、Git はリポジトリに存在するはずの他のファイル(other-project/ 等)が「削除された」と認識し、git diff に大量の削除差分が表示されます。

sparse-checkout により、Git のインデックスから aws/ 以外のエントリが除外され、存在しなくても問題ないものとして扱われます。

試行錯誤の過程

この構成に至るまでに、いくつかのアプローチを試しました。

第1段階:GIT_DIR + GIT_WORK_TREE 環境変数

// ❌ 最初のアプローチ(廃止済み)
"remoteEnv": {
  "GIT_DIR": "/git-dir/.git",
  "GIT_WORK_TREE": "/workspace"
}

問題: 本Gitリポジトリにのみ適用されてほしいのですが、これらの環境変数はすべての子プロセスに伝播します。結果、他のリポジトリを利用したい場合、私の場合は具体的にはTerraform が内部で git コマンドを実行する際(remote module取得時)や、Claude Code がプラグインをインストールする際に失敗しました。

第2段階:gitdir ポインタファイル

# ❌ 中間のアプローチ(廃止済み)
echo "gitdir: /git-dir/.git" > /workspace/.git

.git をファイルとして作成し、実際の Git ディレクトリへのポインタとする方法です。環境変数の問題は解消しましたが、.git/git-dir/ という無関係なパスにマウントしていたため、間接参照が一段増える不必要な複雑さがありました。

第3段階:直接マウント(現在の構成)

.git/workspace/.git に直接マウントすることで、gitdir ポインタファイルの生成が不要になり、Git の標準的なリポジトリ検出がそのまま機能する構成になりました。

まとめ

課題 解決策
.git がワークスペース外 親ディレクトリに .git をバインドマウント
ファイルシステム境界 GIT_DISCOVERY_ACROSS_FILESYSTEM=1
所有者の不一致 git config safe.directory
コンテナ固有の sparse-checkout .git/info をボリュームで上書き
ボリュームで exclude が隠れる ホスト info を別パスにマウントしてコピー
他ディレクトリの削除差分 sparse-checkout で aws/ のみ追跡
環境変数の子プロセス伝播 環境変数ではなくディレクトリ構造で解決

モノレポのサブディレクトリだけを DevContainer で開発する場合、Git のリポジトリ検出メカニズムとDocker のマウント階層を組み合わせることで、ホスト側に影響を与えずにコンテナ内でフルの Git 機能を実現できます。

参考情報

この記事をシェアする

FacebookHatena blogX

関連記事