Dev Container に Terraform MCP Server をインストールする際に Docker を使わないようにした
「Dev Container に docker-outside-of-docker feature を入れてホストの Docker socket を共有するのは、実質ホスト root を渡しているのと同じで危険」という話を書きました。
記事の最後で「じゃあその DooD、本当に必要なのか? から見直そう」と提案して終わっていたので、今回はその続きとして、DooD を使っていた唯一の理由だった terraform-mcp-server を、Docker を介さずに動かす方法を実装した記録を残します。
結論からいうと、以下のような構成変更で DooD を完全に追い出せました。
| Before (DooD 必須) | After (DooD 不要) |
|---|---|
docker run -i --rm hashicorp/terraform-mcp-server:0.4.0 を MCP サーバとして起動 |
terraform-mcp-server stdio バイナリを直接起動 |
docker-outside-of-docker feature が必要 |
Dev Container Feature でビルド時にバイナリを ~/.local/bin/ に配置 |
背景:なぜ Docker 経由になっていたか
hashicorp/terraform-mcp-server の公式ドキュメントには、.mcp.json の例として以下のような Docker 前提の起動コマンドが載っていて、深く考えずそのまま採用していました。
{
"mcpServers": {
"terraform": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "hashicorp/terraform-mcp-server"]
}
}
}
この docker コマンドを Dev Container 内から叩くために docker-outside-of-docker feature を有効化していて、それが前回記事の発端でした。
突破口:prebuilt binary が使える
terraform-mcp-server の prebuilt binary は、HashiCorp の公式リリースサーバ (releases.hashicorp.com) に最初期のバージョンから配布されていました。Docker イメージしか無いと思い込んでいただけで、このバイナリを直接取ってくれば Docker はそもそも不要です。
これを Dev Container ビルド時に ~/.local/bin/ に展開してしまえば、.mcp.json からは Docker を経由せずに直接バイナリを呼び出せます。
{
"mcpServers": {
"terraform": {
"type": "stdio",
"command": "terraform-mcp-server",
"args": ["stdio"]
}
}
}
実装:Dev Container Feature を自作する
Dev Container には Features という仕組みがあり、devcontainer.json と同じリポジトリにローカル Feature を置けます。今回は terraform-mcp-server 専用の Feature を作りました。
ディレクトリ構成
.devcontainer/
├── devcontainer.json
└── features/
└── terraform-mcp-server/
├── devcontainer-feature.json
└── install.sh
devcontainer-feature.json
{
"id": "terraform-mcp-server",
"version": "1.0.0",
"name": "Terraform MCP Server",
"description": "Installs the HashiCorp terraform-mcp-server prebuilt binary.",
"options": {
"version": {
"type": "string",
"proposals": ["1.0.0", "latest"],
"default": "1.0.0",
"description": "Select version of terraform-mcp-server to install."
}
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
]
}
devcontainer.json での有効化
"features": {
// ...
"./features/terraform-mcp-server": {
"version": "1.0.0"
}
}
install.sh(要点のみ)
#!/usr/bin/env bash
set -e
CLI_VERSION="${VERSION:-1.0.0}"
TARGET_USER="vscode"
TARGET_DIR="/home/${TARGET_USER}/.local/bin"
resolve_asset_arch() {
case "$(dpkg --print-architecture)" in
amd64) echo "amd64" ;;
arm64) echo "arm64" ;;
*) echo "unsupported architecture" >&2; exit 1 ;;
esac
}
download_and_install() {
local asset_arch tmp url
asset_arch="$(resolve_asset_arch)"
tmp="$(mktemp -d)"
trap 'rm -rf "${tmp}"' RETURN
url="https://releases.hashicorp.com/terraform-mcp-server/${CLI_VERSION}/terraform-mcp-server_${CLI_VERSION}_linux_${asset_arch}.zip"
curl -fsSL -o "${tmp}/terraform-mcp-server.zip" "${url}"
unzip -q "${tmp}/terraform-mcp-server.zip" -d "${tmp}" terraform-mcp-server
# `install -d` does not apply -o/-g to intermediate parents, so create
# ~/.local explicitly to avoid it being left root-owned.
install -d -o "${TARGET_USER}" -g "${TARGET_USER}" "/home/${TARGET_USER}/.local"
install -d -o "${TARGET_USER}" -g "${TARGET_USER}" "${TARGET_DIR}"
install -m 0755 -o "${TARGET_USER}" -g "${TARGET_USER}" \
"${tmp}/terraform-mcp-server" "${TARGET_DIR}/terraform-mcp-server"
}
check_packages curl ca-certificates unzip
download_and_install
Dockerfile 側で ENV PATH="${PATH}:/home/vscode/.local/bin" を入れておけば、ビルド後の Dev Container でそのまま terraform-mcp-server が叩けます。
ハマったポイント:install -d の -o/-g は中間ディレクトリには効かない
Feature のビルド自体は通って、Dev Container が起動するところまで進むのですが、その後の postCreateCommand で Claude Code をインストールするスクリプトが落ちます。なお、Dev Container への Claude Code インストーラの組み込み方は以下の記事にまとめています。
Setting up Claude Code...
✘ Installation failed
EACCES: permission denied, mkdir '/home/vscode/.local/state'
/home/vscode/.local/state を vscode ユーザーで作ろうとして弾かれている、というメッセージです。
原因は Feature の install.sh のこの行。
install -d -o vscode -g vscode /home/vscode/.local/bin
mcr.microsoft.com/devcontainers/base:ubuntu をベースにしている場合、/home/vscode/.local は 存在しない ところからスタートします。install -d は中間ディレクトリも作ってくれるのですが、GNU coreutils の install -d は -o / -g / -m を最終コンポーネントにしか適用しません。これは man install(1) にも書かれている挙動です。
結果として:
| パス | 所有者 |
|---|---|
/home/vscode/.local |
root:root ← ここが問題 |
/home/vscode/.local/bin |
vscode:vscode |
/home/vscode/.local が root 所有のままだと、後段の Claude Code インストーラ (curl -fsSL https://claude.ai/install.sh | bash) が vscode ユーザーで ~/.local/state/ を作ろうとして弾かれてしまう、というわけです。
修正は単純で、親も明示的に作るだけ。
install -d -o vscode -g vscode /home/vscode/.local
install -d -o vscode -g vscode /home/vscode/.local/bin
「install -d が再帰的に作ってくれるから 1 行で済むだろう」という油断が裏目に出る、典型的な落とし穴でした。Dev Container Feature は コンテナの初期状態に何も無いところに対して最初に書き込む側になりがちなので、~/.local のような汎用パスを扱うときは、自分が初期化責任者になる前提で書く必要があります。
後片付け:DooD 関連の削除
terraform-mcp-server のコンテナレス化に伴って、以下が不要になりました。
.devcontainer/devcontainer.jsonからghcr.io/devcontainers/features/docker-outside-of-dockerを削除.devcontainer/devcontainer-lock.jsonから該当ロックエントリを削除
動作確認
Rebuild Container 後、コンテナ内で:
$ ls -ld /home/vscode/.local /home/vscode/.local/bin /home/vscode/.local/state
drwxr-xr-x 1 vscode vscode 4096 Jun 20 14:40 /home/vscode/.local
drwxr-xr-x 1 vscode vscode 4096 Jun 20 14:40 /home/vscode/.local/bin
drwxr-xr-x 4 vscode vscode 4096 Jun 20 14:41 /home/vscode/.local/state
$ terraform-mcp-server --version
Terraform MCP Server
Version: 1.0.0
Commit:
Build Date: 1970-01-01T00:00:01Z
$ claude --version
2.1.183 (Claude Code)
~/.local 配下が全部 vscode 所有になっていて、terraform-mcp-server と claude がどちらも揃っています。Claude Code 側からも MCP サーバとして問題なく接続できました(mcp__terraform__* ツール群が利用可能になっていることを確認)。
まとめ
- DooD を消すには、そもそも Dev Container 内で Docker を使わずに済む構成にするのが本質的な対処。
- terraform-mcp-server の prebuilt binary は
releases.hashicorp.comにもともと配布されているので、Dev Container Feature 化してビルド時に取得すれば Docker は不要。 install -d -o user -g group <path>で中間ディレクトリの所有権が root のまま残る罠は、Dev Container Feature だけでなくシェルスクリプト全般で踏みうるので覚えておくと良いです。







