devcontainer.jsonで開発環境を1ファイル化する:Codespacesもチームも同じ箱で動かす
「僕のPCでは動く」を消す方法。devcontainer.jsonの書き方、features、postCreateCommand、Codespacesでの再現、Claude Codeを隔離して安全に走らせるコツを実例で。
新メンバーに「環境セットアップ、READMEに書いてあるからよろしく」と言った3時間後。Slackに「Node、18でいいですか?」「psqlって何で入れます?」「拡張機能どれ入れました?」と質問が並んでいました。
READMEには手順を書いたつもりでした。でも手順書は、書いた瞬間から少しずつ古くなる。Nodeのバージョンは上がるし、誰かがこっそり入れたCLIは書き忘れる。「僕のPCでは動く」が、チームの口癖になっていました。
このループを断ち切るのが Dev Container です。開発環境そのものを devcontainer.json という1ファイルに書いて、リポジトリに置く。エディタがそれを読んでコンテナを立て、全員がまったく同じ箱の中で作業する。手順書を読ませる代わりに、環境を配る発想です。
この記事は、Docker Composeで複数サービスを束ねる話ではなく(それは別記事に分けます)、devcontainer.json 1ファイルから始めて、features・postCreateCommand・Codespaces・Claude Codeの隔離までを、僕が実際にハマった順で書きます。
この記事の要点
- Dev Containerは「開発環境をコードにする」仕組み。
devcontainer.jsonをリポジトリに置けば、VS CodeでもCodespacesでも全員が同じ箱で動く。 - まずは
image+featuresだけの最小構成で動く。Dockerfileは後からで十分。 featuresはNodeやGitHub CLIなどを1行で足せる公式パーツ。自前のインストール手順を減らせる。postCreateCommandで初回のnpm ciやPrisma生成を自動化。set -euo pipefailで失敗を隠さない。- Claude Codeをコンテナの中で走らせると、ホストの鍵や別案件のファイルから隔離できる。
remoteUserは非rootにする。
Dev Containerって、要するに何?
Dev Containerは、開発環境をDockerコンテナとして定義し、エディタをその中につなぐ仕組みです。
ふだんアプリを動かすコンテナは「本番用の箱」ですよね。Dev Containerはそれの開発版で、ターミナル、言語サーバー、テスト、Gitクライアント、それにClaude CodeのようなCLIまで、全部この箱の中に入れます。エディタ(VS Code、Cursor、GitHub Codespaces)はホストPCで動きますが、コマンドを叩く先は箱の中。だから箱の中身さえ揃えれば、誰のPCでも同じ結果になります。
たとえるなら、引っ越しのたびに家具をバラ買いするのをやめて、「この部屋まるごと」を持ち運べるようにする感じです。Node 22、psql、ripgrep、お気に入りの拡張機能。これを一度書いておけば、新しいPCでもCodespacesでも、開いた瞬間に同じ部屋ができあがります。
設定ファイルの置き場所は決まっています。リポジトリ直下の .devcontainer/devcontainer.json(または .devcontainer.json)です。エディタはここを最初に読みます。仕様そのものはDev Container Specification(公式)で公開されていて、特定のエディタに縛られないオープンな規格になっています。
まず動かす:image と features だけの最小構成
いきなりDockerfileを書く必要はありません。最初は image と features の2つで動きます。.devcontainer/devcontainer.json をこれだけ書いてみてください。
{
"name": "my-dev",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
"forwardPorts": [3000],
"postCreateCommand": "node --version && npm --version",
"remoteUser": "node",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
}
VS Codeでこのリポジトリを開くと、右下に「Reopen in Container」と出ます。押すだけ。初回はイメージを取りに行くので数分かかりますが、2回目以降はすぐ立ち上がります。コンテナの中でターミナルを開いて node --version を叩けば、v22 が返るはずです。ホストにNodeを入れていなくても、です。
ここで効いているのが各キーの役割です。ざっと並べると、こうなります。
| キー | 役割 | つまずきやすい点 |
|---|---|---|
image | ベースになる開発イメージ | タグを固定しないと再現性が崩れる |
features | Nodeやghなどを足す公式パーツ | バージョン指定はオプションで渡す |
forwardPorts | コンテナのポートをローカルに通す | 外部公開とは別物 |
postCreateCommand | 初回作成後に1回だけ走る | 常駐プロセスを入れると終わらない |
remoteUser | コンテナ内で使うユーザー | rootにすると所有者が崩れる |
customizations.vscode | 拡張機能やエディタ設定 | 全員に強制される点を意識する |
最小構成のいいところは、壊しても戻せることです。まずこれを動かして、必要になったらピースを足す。最初から完璧な設定を目指すと、たいてい途中で力尽きます。
features を使うとインストール手順が消える
僕が最初に書いたDev Containerは、Dockerfileに apt-get install を10行くらい並べた力作でした。GitHub CLIを入れて、Python入れて、AWS CLI入れて……。動いたけど、メンテがつらい。バージョンを上げるたびにインストールスクリプトとにらめっこです。
そこで知ったのが features でした。よく使うツールを「公式の部品」として1行で足せる仕組みです。ghcr.io/devcontainers/features/... の形で指定すると、エディタが裏でインストールスクリプトを走らせてくれます。
たとえばこう書くと、GitHub CLI・Go・特定バージョンのNodeが入ります。
{
"image": "mcr.microsoft.com/devcontainers/base:bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "22" },
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/go:1": { "version": "1.23" }
}
}
ポイントは、{ "version": "22" } のようにオプションを渡せることです。Nodeのバージョンを18にしたければ値を変えるだけ。Dockerfileの中身をいじる必要がありません。利用できる部品はDev Container Features(公式カタログ)に一覧があります。Node、Python、Go、Rust、Docker、AWS/GCP CLIあたりは一通り揃っています。
ただし万能ではありません。featuresは便利な反面、「何が入っているか」がカタログ任せになります。バージョンを厳密に固定したい、社内のプロキシ越しにインストールしたい、独自パッチを当てたい——こういう要求が出てきたら、Dockerfileに切り替えるサインです。最初はfeatures、要件が固まったらDockerfile。この順番が楽でした。
postCreateCommand で初回セットアップを自動化する
コンテナが立ち上がっても、npm install が済んでいなければアプリは動きません。これを毎回手で叩くのは事故のもとです。誰かが忘れて「動かない」と言い出す。
そこで postCreateCommand を使います。コンテナ作成後に1回だけ走るコマンドです。依存インストールやコード生成をここに寄せます。ただし、長いコマンドをJSONの1行に詰め込むとレビューしづらいので、僕はシェルスクリプトに分けています。
devcontainer.json 側はこう。
{
"postCreateCommand": "bash .devcontainer/post-create.sh"
}
中身の .devcontainer/post-create.sh がこちらです。コピペで動きます。
#!/usr/bin/env bash
set -euo pipefail
echo "==> 依存関係をインストール(lockfileを優先)"
if [ -f pnpm-lock.yaml ]; then
corepack enable
pnpm install --frozen-lockfile
elif [ -f package-lock.json ]; then
npm ci
elif [ -f package.json ]; then
npm install
else
echo "package.json が無いのでスキップします"
fi
# Prismaを使うプロジェクトならクライアントを生成
if [ -f prisma/schema.prisma ]; then
echo "==> Prisma client を生成"
npx prisma generate
fi
echo "==> バージョン確認"
node --version
npm --version
1行目の set -euo pipefail が地味に大事です。これを入れておくと、インストールに失敗した時点でコンテナ作成を止めてくれます。これが無いと、依存が入っていない壊れた箱の中で作業が始まり、「なぜか動かない」を延々と追うことになります。
npm install ではなく npm ci を優先しているのも理由があります。npm install は package-lock.json を勝手に書き換えることがある。チームで使うなら、lockfileを尊重する npm ci を初回に回すほうが、全員の依存ツリーが揃います。
ちなみに、ここで npm run dev のような常駐プロセスを入れてはいけません。postCreateCommand はそれが終わるまでコンテナ作成が完了しないので、サーバーを起動すると永遠に終わりません。開発サーバーはターミナルで手動起動するか、VS Codeのtaskに分けます。
GitHub Codespacesで同じ箱を開く
Dev Containerの本領は、ローカルを離れたときに出ます。GitHub Codespacesは、この .devcontainer をクラウド上のマシンで読んで、ブラウザのVS Codeにつないでくれるサービスです。
うれしいのは、ローカルとCodespacesでまったく同じ箱が立つことです。出張先の非力なノートでも、リポジトリのCodespacesを開けば、いつものNode 22とpsqlが入った箱がブラウザに出てきます。「このPCにはDocker入れてないから」が言い訳にならなくなりました。
Codespacesでありがちなのが、秘密情報の渡し方です。ローカルなら .env をうっかり置いても自分しか見ませんが、Codespacesはクラウドです。APIキーはリポジトリやOrganizationのCodespaces secretsに登録し、環境変数として注入します。手順はGitHub Codespaces公式ドキュメントに整理されています。devcontainer.json に平文で書くのだけは絶対にやめてください。
もう一点、Codespacesではマシンのスペックを選べます。重いビルドが多いリポジトリなら4-coreや8-coreを選ぶ。逆に放置すると課金が続くので、使わないCodespaceは停止する癖をつけます。ローカルと違って「電源を切れば終わり」ではない、という意識が要ります。
Claude Codeをコンテナの中に閉じ込めて安全に走らせる
ここからが、僕がDev Containerを手放せなくなった本当の理由です。
Claude Codeのようなエージェント型のツールは、ファイルを読んで、編集して、テストを走らせて、Gitを操作します。便利です。でもホストPCで直接走らせると、ふと不安になる。~/.ssh の鍵、別案件の .env、クラウドの認証ファイル——AIが読むつもりがなくても、ログやツール実行でたまたま見える状態にしたくない。
そこでClaude Codeをコンテナの中だけで動かします。箱には「この案件に必要なものだけ」入れる。ホストの鍵や別案件のファイルは、そもそも箱の中に存在しません。隔離がそのまま安全装置になります。
devcontainer.json にClaude Code向けの設定を足すとこうなります。
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"remoteUser": "node",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {}
},
"mounts": [
"source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume"
],
"containerEnv": {
"DISABLE_AUTOUPDATER": "1"
},
"customizations": {
"vscode": {
"extensions": ["anthropic.claude-code"]
}
}
}
3つだけ押さえてください。
ひとつ目、remoteUser は必ず node のような非rootにします。rootで走らせると、AIが作ったファイルの所有者がrootになり、ホスト側で編集できなくなることがある。それに権限まわりの被害も大きくなります。
ふたつ目、mounts で ~/.claude を named volume にしています。これでコンテナをrebuildしてもClaude Codeのセッションや設定が残ります。${devcontainerId} を入れているのは、プロジェクトごとにvolumeを分けるためです。全案件で同じvolumeを使い回すと、認証情報が混ざる恐れがあります。
みっつ目、features でClaude Code CLIを入れています。チームで「昨日と同じ挙動」を確認したいなら、Dockerfileで @anthropic-ai/[email protected] のようにバージョン固定するのも手です(2026-06-07時点の最新は npm view @anthropic-ai/claude-code version で確認しました)。DISABLE_AUTOUPDATER=1 を入れているのも、rebuildのたびに挙動が変わるのを防ぐためです。
最新の手順や --dangerously-skip-permissions の注意点は、Claude Code公式のDev Containerドキュメントが一次情報です。ひとつ釘を刺すと、「コンテナの中だから何でも安全」ではありません。bind mountしたワークスペースはホストのファイルでもあるので、AIが消したファイルはホストでも消えます。権限スキップを使うなら、Gitで戻せる状態・短時間の検証に限る、と決めておくのが現実的です。
僕がDev Containerでやらかした失敗3つ
正直に書きます。最初のDev Containerは穴だらけでした。
ひとつ目は、ホストの node_modules を共有したこと。WindowsやmacOSで作った node_modules をLinuxコンテナにそのままbind mountしたら、ネイティブモジュールが壊れて謎のエラーが出ました。node_modules はnamed volumeに逃がして、コンテナの中でインストールし直す。初回は少し時間がかかりますが、OS差の事故が消えます。
ふたつ目は、ポートを全部公開したこと。「つながらないから」とDBもRedisも片っ端から外に開いたら、ホストのPostgreSQLと番号が衝突しました。VS Codeから使うだけなら forwardPorts で十分で、これは外部公開とは別物です。本当に外部から繋ぎたい時だけ、明示的にpublishする。この順番にしてから事故が減りました。
みっつ目は、.env を箱に入れて満足したこと。権限設定で守った気になっていましたが、いちばん強いのはそもそも渡さないことです。開発用の DATABASE_URL はコンテナ内サービス向けにして、本番APIキーはCodespaces secretか手動入力に分ける。最初から箱に入れなければ、漏れようがありません。
よくある質問
Q. Dev ContainerとふつうのDockerは何が違う? 本番用のDockerはアプリを動かす箱ですが、Dev Containerは開発するための箱です。エディタが中につながり、ターミナル・拡張機能・言語サーバー・Claude Codeまで箱の中で動かします。目的が「実行」か「開発」かの違いです。
Q. Dockerfileは必須?
いいえ。image と features だけで始められます。バージョンを厳密に固定したい、独自のインストール手順がある、といった要件が出てきてからDockerfileに切り替えれば十分です。
Q. VS Code以外でも使える?
使えます。Dev Containerはオープンな仕様で、Cursor、GitHub Codespaces、JetBrains系など複数のツールが対応しています。devcontainer.json 自体は共通です。
Q. Codespacesは無料? 個人アカウントには毎月の無料枠があり、超えると従量課金です。使わないCodespaceを停止し忘れると課金が続くので、停止の習慣が要ります。詳細はGitHub公式を確認してください。
Q. 複数のDBやサービスをまとめて立てたい場合は?
devcontainer.json 単体でも features で足せますが、アプリ+PostgreSQL+Redisのように複数コンテナを束ねるならDocker Composeが向いています。その設計はClaude Code Docker Compose開発環境ガイドに分けて書きました。
実際に試した結果
冒頭の「Slackに質問が並ぶ問題」は、devcontainer.json を1枚置いただけでほぼ消えました。新メンバーには「リポジトリを開いてReopen in Containerを押して」とだけ伝えればいい。Nodeのバージョンも拡張機能も、聞かれなくなりました。
いちばん効いたのは、凝った設定ではなく小さく始めたことです。最初は image + features + postCreateCommand の3点だけ。動いてから、Claude Codeの隔離やvolume分離を足していきました。一方で、欲張って最初からDocker Composeで全部組もうとした別リポジトリは、設定が複雑すぎて誰もメンテしなくなりました。
環境をコードにすると、「僕のPCでは動く」が「リポジトリで動く」に変わります。手順書を更新し続ける消耗から解放されるだけでも、書く価値がありました。導入の全体像はClaude Code開発環境セットアップに、検証をCIに載せる流れはClaude Code CI/CDセットアップガイドにまとめてあります。
チームのDev Container設計や権限ルールの整備で詰まったら、研修・相談でテンプレートごと一緒に組み立てられます。まずは最小の devcontainer.json を1枚、リポジトリに置くところから始めてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。