Grandream

Grandream

公開: 12 min read

エージェントの実行環境をmicroVMへ|Docker Sandboxes初期設定

エージェントの実行環境をmicroVMへ|Docker Sandboxes初期設定
AIエージェントの開発をご検討中ですか? AIエージェント開発サービスを見る →

連載「Docker Sandboxesでコーディングエージェントを隔離する」第1回/検証日: 2026年8月23日/sbx 0.39.0

あるプロジェクトの開発環境を、Dev ContainerからDocker Sandboxesへ移しました。Node.js、開発サーバー、Codex、Claude Codeをまとめてホストの外へ出しました。手元に残ったのはshellとsbxだけ。

サンドボックスを1つ起動するコマンド自体は一行で済みます。手間がかかったのはその外側、つまりホストとmicroVMの間に引く境界の設計でした。この連載では、その境界をテーマごとに分けて書きます。第1回は入り口として、Dev Containerから移した理由・Kitの書き方・outboundの絞り方・人とエージェントの分担を扱います。

なお、Docker Sandboxesそのものを導入すべきかという判断軸は、別記事「Docker Sandboxes (sbx) 実務導入ガイド — Dev Containers から乗り換える価値」で整理しました。本連載はその先、実際に1つのプロジェクトへ入れたときの設定と実測に寄せています。

なお、アプリ名や環境変数名は公開用の仮名(sampleappAPP_*)です。sbxはDocker Sandboxes公式のCLI名なのでそのまま表記します。

Dev Containerをやめたのは、隔離の強度とIDEの価値が同時に動いたから

このプロジェクトの開発環境は、これまでDev Containerでした。IDEからコンテナへ入り、その中で依存を解決してビルドを回す、という一般的な形です。移行の理由は2つあります。

1つ目は隔離の強度です。 Dev Containerはコンテナなので、ホストとカーネルを共有します。ファイルシステムやプロセスは分かれていても、境界はカーネル1枚です。人が書いたコードを人が実行する分にはこれで十分でした。前提が変わったのは、エージェントに事前承認をほとんど求めずコマンドを実行させる運用に切り替えたときです。何を実行するか事前に読まない前提なら、境界は厚いほうがいい。Docker Sandboxesの各サンドボックスはmicroVMで、カーネルもネットワークもDocker daemonも別に持ちます。加えてcredentialをホスト側のproxyで預かる仕組みがあるので、鍵そのものを渡さずにAPIを使わせられます(第3回・第4回)。

ホスト上で直接走らせる場合、選択肢は2つしかありません。広い実行権限を与えてホストのファイル・認証情報・ツールを射程に入れるか、承認を細かく挟んで待ち時間を増やすか。microVMはこの二択から抜けるための箱です。実際の運用では、サンドボックス内のエージェントに事前承認をほとんど求めていません。壊れても作り直せる場所だから、そこまで振り切れます。

2つ目は、IDE統合の価値が下がったことです。 Dev Containerの旨みの大半は、IDEがコンテナ内の言語サーバー・拡張機能・デバッガをそのまま使えることにあります。エディタで文字を打つ人がいて初めて効く価値です。ところが実際の作業は、コード生成もテスト実行も修正もエージェントが回すようになり、人の仕事はレビューと承認へ寄りました。補完が手元のエディタで効くかどうかは、もうほとんど効用がありません。

代わりに要るものがはっきりしました。任意のコマンドを安全に実行できる箱と、エージェントがそこへ入る口です。前者がmicroVM、後者がSSH経由の接続(第5回)で、どちらもDev Containerが得意な領域ではありません。

Dev Containerが劣るという話ではありません。人が書く前提なら今も妥当な選択です。前提が動いたので、道具を替えました。

clone modeは、Gitを唯一の受け渡し口にする仕組み

今回はclone modeを使いました。ホストのリポジトリはサンドボックス内へ読み取り専用でマウントされ、編集・依存インストール・Git操作はmicroVM内のプライベートcloneで行われます。

実務上ありがたいのは、プラットフォームの違いが混ざらない点です。Linux用にインストールしたnode_modulesが、ホストのmacOS用node_modulesを踏み潰す事故は起きません。ネイティブモジュールを含むプロジェクトほど効きます。

ただし、この分離には代償があります。

  • ホスト側の未コミット変更は、作成済みのプライベートcloneへ自動では入らない
  • サンドボックス側の変更も、ホストのworking treeへ自動では戻らない
  • サンドボックスを削除すると、その中にしかないcommitや未コミット変更も消える

つまり両者の受け渡し境界はGitひとつです。「ホストで直したのにサンドボックスに反映されない」も「サンドボックスで直したのにホストに無い」も、仕様どおりの挙動です。commitとpushを挟む、という運用がそのまま前提になります。

この性質は、サンドボックスを作り直すときに一番痛い形で出ます。Kitを変更して環境を作り替える場面が連載の後半に何度か出てきますが、そのたびに未pushの取りこぼしを確認する手順が必要になります(第5回で扱います)。

もうひとつの選択肢、つまりホストのworking treeをそのまま共有するmount方式は採りませんでした。macOSのVirtioFS上でLinuxのnpmを動かすとnode_modulesの大量の小さいファイル操作と相性が悪く、実際にディレクトリ並列作成でENOTDIRを踏んだためです。この比較と最終的な構成判断は第6回で扱います。

置くものはspec.yamlひとつから始める

リポジトリ側に足すのは、最初はこれだけです。

.
├── .nvmrc
├── .sbx/
│   └── sampleapp/
│       └── spec.yaml
└── package.json

.sbx/sampleapp/spec.yamlが共通Kit、つまりこの環境のsource of truthになります。エージェントを増やすとき(第2回)も、credentialを足すとき(第3回・第4回)も、書き換えるのは基本このファイルです。

CLI自体はHomebrew経由で入ります。現在の公式要件はApple siliconとmacOS Sonoma 14以降ですが、要件は変わり得るので実行前に公式ページも見てください。

brew trust docker/tap
brew install docker/tap/sbx

sbx --version
sbx login
sbx daemon status

すでに入っているならbrew upgrade docker/tap/sbxで更新します。sbx daemon statusまで通れば、あとはKitを書く作業です。

Kitはmixinで書き、install処理は冪等にする

第1回の時点で書くのは、ネットワークとNode.jsだけです。

schemaVersion: "2"
kind: mixin
name: sampleapp
version: 1.0.0
displayName: Sample application development

permissions:
  network:
    allow:
      - "**"

setup:
  install:
    - description: Install the pinned Node.js version
      user: "0"
      command: |
        set -eu

        expected_node="v22.12.0"
        if [ "$(node --version 2>/dev/null || true)" = "$expected_node" ]; then
          exit 0
        fi

        apt-get update
        apt-get install -y --no-install-recommends ca-certificates curl xz-utils

        case "$(dpkg --print-architecture)" in
          amd64) node_arch="x64" ;;
          arm64) node_arch="arm64" ;;
          *)
            echo "Unsupported architecture: $(dpkg --print-architecture)" >&2
            exit 1
            ;;
        esac

        node_archive="node-v22.12.0-linux-${node_arch}.tar.xz"
        curl --fail --location --silent --show-error \
          "https://nodejs.org/dist/v22.12.0/${node_archive}" \
          --output "/tmp/${node_archive}"
        tar --extract --xz --file "/tmp/${node_archive}" \
          --directory /usr/local --strip-components=1
        rm -f "/tmp/${node_archive}"

        test "$(node --version)" = "$expected_node"
        test "$(npm --version | cut -d. -f1)" = "10"

kind: mixinにすると、CodexやClaudeの組み込みエージェントKitを土台に残したまま、プロジェクト共通の設定を足せます。エージェント環境を自前で全部組み直す必要はありません。

install処理は再実行され得るので、**冒頭で期待バージョンを確認して早期にexit 0**します。冪等にしておかないと、作り直すたびに数分待つことになります。

末尾には検証を置きます。node --versionnpmのメジャーバージョンをテストしておけば、archiveの取り違えや展開失敗が、後続のビルドエラーではなくinstall時点で落ちます。上の例は簡略化していますが、実運用ではNode.js公式のchecksumも固定し、展開前にarchiveを検証しています。

書いたKitは、使う前に必ず検証します。

sbx kit validate ./.sbx/sampleapp

Kit schemaはまだ変化の途中にあります。古い記事のフィールド名をそのまま写すと通らないことがあるので、手元のバージョンで検証してから使ってください。しかも検証を通ることと実行時に動くことは別問題で、第4回ではKit referenceどおりに書いて通らなかった例を扱います。

credentialはまだ書きません。GitHubのトークンは組み込みのservice(sbx secret set github)で足りるのでKitへの追記が不要で、アプリ用のAPIキーだけをcredentials:ブロックとして第4回で足します。

outboundは閉じて始め、必要なdomainだけ開ける

permissions.network.allowの書き方は、この構成で一番迷いました。

allow: ["**"]はサンドボックスから外へ出る通信を全開放する設定で、今回は開発の自由度を優先してこれを選びました。ただし、これは検証しながら緩めた結果であって、初期設定としての推奨ではありません。最初はoutboundを閉じ、通らなかったdomainを1つずつ許可していくほうが、環境の依存先が可視化されて後で効きます。

閉じた状態から始めると、npm ciが落ちた時点でパッケージregistryが要ると分かり、ghが落ちた時点でGitHubのAPI/HTTPSが要ると分かります。許可リストは、そのプロジェクトが外部へ何を要求しているかの一覧そのもの。全開放から始めると、この情報は手に入りません。エージェントに事前承認をほとんど求めない運用なら、なおさら価値があります。任意のendpointへデータを送れる状態は、ファイルシステムを隔離しても閉じないからです。

もうひとつ、allow: ["**"]が何を守らないかも押さえておく必要があります。

  • これはoutboundの設定で、インターネットからサンドボックスへ入れるようになる設定ではありません
  • 外部UDPとICMPはそもそも制御対象外です
  • 組織ポリシーが設定されている環境では、組織側の制限が優先されます

「全開放したのにpingが通らない」「ポリシーで弾かれる」はこの3点で説明できます。

Sandboxはプロジェクトに1つ、npm ciまで通す

Kitが検証を通ったら、サンドボックスを作ります。この連載ではプロジェクトに1つの永続Sandboxを立て、CodexとClaude Codeの両方をその中で動かします(同居のさせ方は第2回、その構成を選んだ理由は第6回)。

sbx create \
  --name sampleapp \
  --clone \
  --kit ./.sbx/sampleapp \
  --publish 127.0.0.1:3000:3000/tcp4 \
  claude .

sbx exec sampleapp npm ci

オプションの意図はこうです。

オプション

意図

--name sampleapp

SSH hostがsampleapp.sbx、ホスト側Git remoteがsandbox-sampleappになる

--clone

microVM内のプライベートcloneを作業先にする

--kit ./.sbx/sampleapp

共通のtoolchainとネットワーク/credentialポリシーを適用する

--publish 127.0.0.1:3000:3000/tcp4

開発サーバーをホストのloopbackだけに公開する

claude .

Claude agent typeで現在のリポジトリを対象にする

末尾のclaudeがagent typeです。Codexではなく Claude を選ぶのは、Claude DesktopのSSH連携がこのagent typeを要求するからで、Codex CLIは第2回でKitから足します。

publishを127.0.0.1へ限定すると、同一LANや外部interfaceへ開発サーバーが漏れません。tcp4を明示しているのは、macOSでlocalhost::1を先に解決するのに対しサンドボックス内のアプリが0.0.0.0(IPv4)でlistenしていると繋がらない、という食い違いを避けるためです。

npm ciが通ったら、意図した場所に環境ができているか確認します。

sbx exec sampleapp sh -lc '
  node --version
  npm --version
  findmnt -T "$(git rev-parse --show-toplevel)" -o FSTYPE,TARGET -n
  findmnt -T /run/sandbox/source -o FSTYPE,OPTIONS -n
'

見たいのは2行です。プロジェクトルートがext4(microVM内のLinux filesystem)であること、そして**/run/sandbox/sourcevirtiofsのread-only**であること。前者が作業先のプライベートclone、後者がホストリポジトリの読み取り専用マウントです。ここが逆になっていたら、clone modeで作れていません。

node_modulesもこのLinux filesystem側に置かれます。サンドボックスを停止しても残るので、npm ciはlockfileを変えたとき、依存が壊れたとき、サンドボックスを作り直したときだけです。

自動化しない仕事を、先に決める

初期設定でもうひとつ効いたのが、人とエージェントの分担を最初に決めたことです。全部自動化しようとすると、認証とホストdaemonの境界で必ず詰まります。

人がホストで行うこと

Codex/Claudeに任せられること

sbxのインストールと更新

Kit、ランチャー、ドキュメントの編集

Dockerアカウントへのログイン

Kitとシェルスクリプトの静的検証

ブラウザを使うOAuth認証

サンドボックス内でのnpm ci、lint、build

PATやAPIキーを対話promptへ貼り付ける

credentialの存在確認とAPI疎通確認

ホスト側daemonが異常停止したときの再起動

開発サーバー起動、ログ確認、UI修正

サンドボックス削除前の最終確認

GitHub CLIを使ったIssue/PR操作

左側に共通するのは、ブラウザ・Keychain・ホストのdaemonという、microVMの外側にある資源です。daemon周りは特に境界がはっきり出ます。PID・socket・logはmacOSのユーザーライブラリ配下に置かれるため、デスクトップアプリ経由でエージェントを動かしている場合(第5回)、アプリ側のファイルアクセス制限でsbx daemon restartを完遂できません。詰まったら人がホストの通常ターミナルで打ちます。

sbx daemon restart
sbx daemon status

右側、つまりサンドボックス内で完結する作業はほぼ任せられます。npm ciもlintもbuildも開発サーバーの起動も、失敗しても影響はmicroVMの中で止まる。ここが隔離の見返りです。

まとめ

第1回の要点です。

  • Dev Containerから移した理由は2つ。カーネル1枚より厚い境界が要ること、そして人が書かなくなってIDE統合の価値が落ちたこと
  • 隔離の価値は、エージェントの権限を絞ることではなく、広い権限を渡せる場所を用意することにある
  • clone modeでは、ホストとサンドボックスの受け渡し境界がGitひとつになる。反映されないのは仕様
  • Kitはmixinで書き、install処理は冪等に、バージョン検証は末尾に置く。使う前にkit validateを通す
  • outboundは閉じて始め、通らなかったdomainだけ開ける。許可リストが依存先の一覧になる
  • allow: ["**"]はinboundを開けない。UDP/ICMPは対象外、組織ポリシーが優先
  • Sandboxはプロジェクトに1つ。claude agent typeで作り、--publish 127.0.0.1:3000:3000/tcp4でloopbackだけに出す
  • 作った後はfindmntでプロジェクトルート=ext4/run/sandbox/source=virtiofs read-onlyを確認する
  • ブラウザ・Keychain・ホストdaemonに触る作業は人、microVM内で完結する作業はエージェント

次回は、この1つのSandboxへCodex CLIを足してClaude Codeと同居させます。agent typeの選び方、Codex側の認証、そして増えたコマンドをひとつのshell launcherへ集約する話です。

連載の構成

  1. Docker Sandboxes入門とプロジェクト初期設定(本記事)
  2. CodexとClaudeを同じSandboxに同居させる
  3. GH_TOKENでリポジトリ権限を絞る
  4. OpenAI APIアクセスのはまりどころ(header注入)
  5. Codex AppとClaude DesktopからSSHで入る
  6. Sandboxを1つにした構成判断
  7. Orca ADEとSSH接続の並列開発(別記事)

参考資料


本記事はsbx 0.39.0で2026年8月23日に検証した内容です。CLIのオプション名・Kitのschema・要件は更新される可能性があるため、導入時は最新の公式ドキュメントを併せて確認してください。

関連記事

Grandream

Grandream

株式会社グランドリーム

AI・システム開発のプロフェッショナルチームです。AIエージェント・業務自動化・Webシステム開発などを手がけています。

AIエージェント開発のご相談はお気軽に

PoC段階から本番運用まで一貫対応します。

AI開発について相談する

AIエージェント開発サービスの詳細を見る →

どのサービスが合うか30秒で診断する →