Claude Code を devcontainer で動かすマルチリポジトリ開発環境の構築

はじめに

Claude Code のようなコーディングエージェントに長めのタスクを任せるとき、実行のたびに許可プロンプトへ応答するのは手間が大きいと思います。一方で、許可プロンプトを外した運用(--dangerously-skip-permissions)をホストマシン上で行うのは避けたいところです。

この記事では、devcontainer を使って次の 5 点を満たす実行環境を構築します。

  • Claude Code をコンテナ内で実行し、許可プロンプトなしの運用を許容できる隔離を確保する
  • 複数リポジトリを横断して参照・編集できる
  • エージェントが gh コマンドで PR 作成や issue 操作を行える
  • MCP(AI がツールや外部サービスへ接続するための共通規格)で接続できる
  • 各リポジトリの開発環境を docker compose で起動できる

前提環境は以下の通りです。

  • macOS + Docker Desktop
  • VS Code + Dev Containers 拡張
  • Claude Code のサブスクリプションまたは API アクセス

構成の設計判断

ここでは、構成要素ごとに何を選び、なぜそうしたかを整理します。

devcontainer は各リポジトリではなく親フォルダに置く

devcontainer の仕組みは単純で、VS Code は「開いたフォルダ直下の .devcontainer/devcontainer.json」を読むだけです。これを利用して、各リポジトリではなく、リポジトリ群を束ねる親フォルダに設定を置きます。

~/Repos/
├── .devcontainer/
│   └── devcontainer.json
├── product-a/   (git リポジトリ)
├── product-b/   (git リポジトリ)
└── product-c/   (git リポジトリ)

VS Code で ~/Repos を開いてコンテナを起動すると、コンテナ内の /workspaces/Repos 配下に全リポジトリがマウントされます。Claude Code から「product-a の API 定義を参照しながら product-b のクライアントを修正する」といったリポジトリ横断の作業がそのまま可能になります。

副次的な利点として、devcontainer の設定がどのリポジトリにも属さないため、チームのリポジトリに個人環境の設定ファイルを持ち込まずに済みます。

Docker 実行は docker-in-docker を選ぶ

コンテナ内で docker compose を使うには、コンテナ内から Docker を操作できる必要があります。devcontainer の公式 feature には 2 つの選択肢があります。

  • docker-outside-of-docker(ホストの Docker ソケットをマウントする)
  • docker-in-docker(コンテナ内に独立した Docker デーモンを立てる)

今回は docker-in-docker を選びます。ソケットマウントの方が軽量ですが、コンテナ内からホストの Docker を直接操作できてしまうため、「エージェントの実行範囲をコンテナに閉じ込める」という今回の目的と両立しません。docker-in-docker であれば、compose で立ち上がるコンテナ群もすべて devcontainer の内側に閉じます。

MCP は user スコープで登録する

Claude Code の MCP サーバー登録には複数のスコープがあります。よく紹介されるのはリポジトリ直下に .mcp.json を置くプロジェクトスコープですが、これは設定ファイルをリポジトリにコミットしてチームで共有する前提の仕組みです。

今回は user スコープで登録します。設定は Claude Code の設定ディレクトリ側に保存されるため、リポジトリのファイルに影響しません。設定ディレクトリはボリュームで永続化するので、コンテナを作り直しても登録は残ります。

Notion や Figma はいずれも公式のリモート MCP サーバーを提供しているため、コンテナ内にサーバープロセスを立てる必要はなく、URL の登録と OAuth 認証だけで接続できます。

ベースイメージは LTS タグで固定する

ベースイメージには mcr.microsoft.com/devcontainers/base:noble(Ubuntu 24.04 LTS)を使います。

よく見かける base:ubuntu という浮動タグは「その時点の最新 Ubuntu」を指すため、新しいリリースが出た直後は各 feature 側のパッケージ対応が追いついておらず、インストールに失敗することがあります。実際に :ubuntu のまま構築すると、claude-code feature の Node.js 自動インストールや docker-in-docker feature の moby パッケージ導入が失敗するケースがあります。環境の再現性という devcontainer 本来の目的からも、LTS のコードネームで固定しておくのが無難です。

認証情報はボリュームで永続化する

devcontainer はリビルドするとホームディレクトリの内容が消えます。何もしないと、リビルドのたびに Claude Code と gh のサインインをやり直すことになります。

これを避けるため、認証情報の保存先に Docker の名前付きボリュームをマウントします。対象は 3 箇所です。

  • ~/.claude(Claude Code の認証トークン・設定・MCP 登録)
  • ~/.config/gh(gh の認証トークン)
  • /var/lib/docker(docker-in-docker のイメージキャッシュ。認証ではありませんが、リビルドごとの pull し直しを避けられます)

完成形の devcontainer.json

~/Repos/.devcontainer/devcontainer.json として以下を配置します。

{
  "name": "claude-multi-repo",
  "image": "mcr.microsoft.com/devcontainers/base:noble",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {},
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "mounts": [
    "source=claude-config,target=/home/vscode/.claude,type=volume",
    "source=gh-config,target=/home/vscode/.config/gh,type=volume",
    "source=dind-data,target=/var/lib/docker,type=volume"
  ],
  "containerEnv": {
    "CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
  },
  "remoteUser": "vscode",
  "postCreateCommand": "sudo chown -R vscode:vscode /home/vscode/.claude /home/vscode/.config/gh"
}

いくつかの行には理由があるので補足します。

node feature を明示的に入れています。 Claude Code は npm 経由でインストールされるため Node.js と npm が必要です。claude-code feature には Node の自動インストール機構がありますが、ベースイメージによっては失敗することがあるため、node feature で確実に入れておきます。feature 間の依存関係は自動で解決され、node が先にインストールされます。

CLAUDE_CONFIG_DIR を設定しています。 Claude Code の設定・認証の保存先をボリュームのマウント先に向けるための環境変数です。これにより、user スコープの MCP 登録を含む状態がリビルド後も保持されます。

postCreateCommand で chown しています。 Docker の名前付きボリュームは、初回作成時に root 所有で初期化されます。そのまま vscode ユーザーで gh auth login などを実行すると、トークンの書き込みが permission denied で失敗します。コンテナ作成後に所有者を付け替えることで、この問題を回避します。冪等な処理なので、何度実行されても害はありません。

remoteUser は非 root にしています。 Claude Code の CLI は root で起動された場合に --dangerously-skip-permissions を拒否する仕様のため、許可プロンプトなしの運用にはこの設定が前提になります。devcontainers の base イメージに用意されている vscode ユーザーをそのまま使います。

構築手順

ここからは実際の手順です。各手順に完了確認のコマンドを添えます。

1. 親フォルダとリポジトリの準備

mkdir -p ~/Repos && cd ~/Repos
git clone https://github.com/your-org/product-a.git
git clone https://github.com/your-org/product-b.git

clone は HTTPS で行います。コンテナ内の git 認証は後述の gh に統合するため、SSH 鍵をコンテナへ持ち込む必要がなくなります。

2. devcontainer.json の配置

前セクションの内容を ~/Repos/.devcontainer/devcontainer.json として保存します。

cat ~/Repos/.devcontainer/devcontainer.json | python3 -m json.tool

JSON として妥当かをこの時点で確認しておくと、起動時の切り分けが楽になります。

3. コンテナの起動

VS Code で ~/Repos を開き、右下に表示される通知から「Reopen in Container」を選びます。表示されない場合はコマンドパレット(Cmd+Shift+P)から「Dev Containers: Reopen in Container」を実行します。

初回は feature のダウンロードとビルドが走るため、数分から 10 分程度かかります。起動後、コンテナ内のターミナルで確認します。

ls /workspaces/Repos   # 全リポジトリが見えること
docker ps              # docker-in-docker のデーモンが応答すること

4. Claude Code のサインイン

コンテナ内のターミナルで claude を実行し、ブラウザでの認証を完了させます。ブラウザ側は成功しているのにターミナルへ戻らない場合は、ブラウザに表示されるコードをターミナルの入力欄へ手動で貼り付けます(ポートフォワードの経路によってコールバックが届かないことがあります)。

5. gh のセットアップ

gh auth login
gh auth setup-git
gh auth status   # 確認

gh auth setup-git により、git の push / pull も gh の認証で通るようになります。

エージェントに書き込み操作をさせる前提なので、認証には fine-grained personal access token の利用を推奨します。対象リポジトリを限定し、権限は Contents / Pull requests / Issues の Read and write に絞っておくと、意図しない操作があってもトークンのスコープが被害の上限になります。

6. MCP サーバーの登録

user スコープで 2 サービスを登録します。

claude mcp add --scope user --transport http notion https://mcp.notion.com/mcp
claude mcp add --scope user --transport http figma https://mcp.figma.com/mcp

登録後、claude を起動して /mcp コマンドを実行すると各サーバーの認証状態が表示されるので、そこから OAuth を通します。

動作確認チェックリスト

環境が意図通りにできているか、以下を一巡して確認します。

  • どれかのリポジトリで docker compose up -d が通り、VS Code のポートフォワード経由でホストのブラウザからアクセスできる
  • gh issue list -R your-org/product-a で参照系が通る。テスト issue の作成とクローズで書き込み系も確認できるとより確実
  • /mcp で 2 サービスが接続済みになっており、たとえば Notion の記事を 1 件取得できる
  • Claude Code に「product-a と product-b の README を比較して」のようなリポジトリ横断の質問を投げ、両方を読めている
  • 「Dev Containers: Rebuild Container」を一度実行し、claude / gh / MCP の認証がすべて残っている

最後の項目はボリューム設定の検証を兼ねています。ここで認証が消えるようであれば、mounts か CLAUDE_CONFIG_DIR の設定を見直してください。運用に入ってから気づくより、この時点で潰しておく方が安全です。

制約と補足

最後に、この構成が何を割り切っているかを書いておきます。

ネットワークの外向き制限は入れていません。 Anthropic の公式リファレンス実装には、許可ドメイン以外への外向き通信を遮断するファイアウォールスクリプトが含まれています。今回は MCP 2 サービス、gh、compose のイメージ取得先まで許可リストを管理する運用コストを考えて省略し、事故対策は PAT の権限スコープなどトークン側に寄せました。より強い隔離が必要な場合は、公式リポジトリの init-firewall.sh を組み込む選択肢があります。

影響範囲は全リポジトリに及びます。 マルチリポジトリを 1 コンテナに載せる構成のため、許可プロンプトなしで運用した場合の影響範囲もマウントした全リポジトリに広がります。信頼できるリポジトリで使うこと、というのは公式ドキュメントの注意書きの通りです。

docker-in-docker はディスクを消費します。 イメージやビルドキャッシュがコンテナ側のボリュームに蓄積するため、ホストの Docker とキャッシュは共有されません。ときどき docker system prune を実行するか、ボリュームのサイズを気にかけておく必要があります。

隔離の強度と日々の使い勝手はトレードオフの関係にあり、この構成はその中間のバランスを取ったものです。運用してみて不安が残る箇所があれば、ファイアウォールの追加やリポジトリの分割など、締める方向の調整はいつでもできます。

Lima VM 内に Claude Code を導入して認証する

macOS ホストから隔離した Lima VM を作った後、次に必要になるのが coding agent の導入です。この記事では、Lima VM 内に Claude Code を導入し、Claude の subscription を使って認証するまでの手順を紹介します。

ここでは、次の状態まで完了していることを前提にしています。

  • Apple Silicon Mac 上で Lima VM を起動済み
  • VM 内に rootless Docker と基本的な開発ツールを導入済み
  • ホストの home directory や Docker socket は VM に共有していない

VM の作成と rootless Docker の設定は、こちらの記事を参照してください。

tsyknsr.hatenablog.com

なぜ Claude Code も VM 内に入れるのか

Claude Code は、プロジェクトのファイルを読み書きし、shell command を実行します。そのため macOS ホストで直接実行すると、agent がホスト上のファイルや local tool にアクセスできる範囲が広くなります。

Claude Code を Lima VM 内で実行すれば、agent が主に扱う対象を VM の disk、VM 内の Docker daemon、VM に渡した認証情報に限定できます。これは完全な sandbox ではありませんが、macOS ホストとの境界を一つ増やす実用的な方法です。

VM に入る

まず、macOS の terminal から VM に入ります。

limactl shell agent-dev

Lima 2.1 以降、Linux guest の通常ユーザーの home directory は /home/<ユーザー名>.guest です。たとえばユーザー名が your-name なら、home directory は /home/your-name.guest になります。~ はこの VM 専用の home directory を指すため、macOS の /Users/your-name とは別の場所です。

Claude Code を導入する

Claude Code の公式 native installer を VM 内で実行します。

curl -fsSL https://claude.ai/install.sh | bash

native installer は CLI を ~/.local/bin/claude に配置します。インストール後に claude command が見つからない場合は、Bash の PATH に ~/.local/bin を追加してください。

printf '\n# Claude Code native CLI\nexport PATH="$HOME/.local/bin:$PATH"\n' >> ~/.bashrc
source ~/.bashrc

claude --version

今回確認できた version は次のとおりでした。

2.1.206 (Claude Code)

Claude subscription で認証する

VM 内で次の command を実行します。

claude

初回起動時は、テーマと login method の選択画面が表示されます。Claude Pro、Max、Team、Enterprise を利用している場合は、Claude account with subscription を選択します。

VM には通常 GUI browser がないため、Claude Code が browser を自動で開けない場合があります。その場合は画面の案内に従い、c を押して表示された一時的な認証 URL をコピーします。

  1. URL を macOS ホストの browser で開きます。
  2. Claude account でログインします。
  3. browser に表示された認証コードを、VM 内で待機している Claude Code の terminal に直接貼り付けます

認証コードや一時 URL は credential と同様に扱ってください。shell history、設定ファイル、issue、チャットには残さないようにします。

認証が完了したら、Claude Code 内で /status を実行して account と認証状態を確認できます。

認証情報をどこに保存するか

Linux 上の Claude Code は、認証情報を VM ユーザーの ~/.claude/.credentials.json に保存し、ファイル mode を 0600 に設定します。これは VM の disk にだけ保存され、macOS の Keychain やホストの home directory は使用しません。

ただし、VM 内で Claude Code を実行できる agent は、その認証情報を使って Claude API にアクセスできます。したがって、VM を「認証情報を持つ実行環境」として扱い、ホストで普段使用している SSH 鍵や権限範囲が広い API key を追加で渡さないことが重要です。

よくある確認ポイント

claude: command not found と表示される

~/.local/bin が PATH に入っていない可能性があります。次を実行してから、もう一度確認してください。

export PATH="$HOME/.local/bin:$PATH"
claude --version

恒久的な設定は前述の .bashrc への追加で行います。

認証後に Not logged in と表示される

claude を再実行し、login flow を最初からやり直してください。browser で発行される URL とコードは一時的なものです。以前の URL やコードを再利用せず、最新のものを VM 内 terminal に貼り付けます。

認証コードの貼り付け後は、Claude Code が通常の入力画面に戻るまで待ってください。認証処理中に terminal を閉じたり、Ctrl-C で終了したりしないことも大切です。

この時点で渡していないもの

Claude Code の導入と認証が終わった時点でも、次のものは VM に渡していません。

  • macOS ホストの home directory
  • ホストの Docker socket
  • ホストの SSH 鍵と SSH agent
  • GitHub の token や deploy key
  • プロジェクトの workspace mount

GitHub 認証は、次の段階で VM 専用・repository 単位の read-only deploy key から追加する予定です。最初から権限範囲が広い credential をまとめて入れないことで、問題が発生した場合の影響範囲と調査範囲を抑えられます。

まとめ

Claude Code は macOS ホストではなく Lima VM 内に導入し、認証情報も VM 内に限定します。browser login は macOS 側で行いますが、認証コードは VM の Claude Code terminal に直接戻します。

この手順により、agent が実装作業で必要とする Claude Code の機能を利用しながら、ホストのファイルや Docker daemon をそのまま渡さない構成にできます。

参考

Codex / Claude Code を Lima VM に隔離して Docker 開発環境を作る

AI coding agent に実装、テスト、Docker Compose の起動まで任せたい場面は増えています。一方で、macOS のホームディレクトリ、SSH 鍵、API key、ホストの Docker daemon までそのまま渡すと、意図しない変更や認証情報の流出につながるおそれがあります。

この記事では、Apple Silicon Mac 上で Lima VM を作り、その中に rootless Docker と開発ツールを配置する手順を紹介します。最初の段階では、Codex / Claude Code の認証情報も、ホストのプロジェクトディレクトリも VM に渡しません。

目標と前提

目的は、agent に実装作業に必要な権限を渡しながら、macOS ホストへの影響範囲を小さくすることです。ここでいう agent は、Codex や Claude Code のようにファイル編集やコマンド実行を行うツールを指します。

macOS host
└─ Lima VM (agent-dev)
   ├─ Codex / Claude Code        # 次段階で導入
   ├─ git / node / python / go
   ├─ rootless Docker daemon
   └─ app / db / test containers

重要なのは、Docker socket を使う場合でも VM 内の socket だけに限定することです。Docker socket は Docker daemon を操作するための接続口です。ホストの /var/run/docker.sock を渡すと、agent は実質的にホスト Docker を自由に操作できます。これは今回の隔離目的に合いません。

この構成で守りたい主な境界は、agent と VM の間ではありません。agent が Docker を操作できる場合、VM 内では強い権限を持ちます。そこで、VM と macOS ホストの間 を防御の中心にします。

なぜ Lima を選んだか

Docker Compose や Dockerfile を前提とする既存プロジェクトと、そのまま使いやすいことを優先しました。Apple container の中でさらに Docker を常用すると、Docker-in-Docker という入れ子の構成になります。この構成では、ネットワーク、volume、権限、デバッグの扱いが複雑になりやすいためです。

Lima は、macOS 上に Linux VM を作成できるツールです。Apple Silicon Mac では vmType: vzmountType: virtiofs を指定すると、macOS の Virtualization.framework を利用できます。Docker は VM 内だけに置き、ホストの Docker には接続しません。

最初に作った VM

まずは、ホストとの共有 mount を完全に無効化した VM を作ります。mount は、ホストのディレクトリを VM から見えるようにする機能です。認証情報、ソースコード、ホームディレクトリの共有は、必要性を確認してから最小限だけ追加してください。

brew install lima

limactl validate agent-dev.yaml
limactl create --name=agent-dev agent-dev.yaml
limactl start agent-dev
limactl shell agent-dev

作成した agent-dev.yaml は次のとおりです。CPU、メモリ、disk の値は、普段扱うプロジェクトの規模に合わせて調整してください。

minimumLimaVersion: "2.0.0"

base:
  - template:_images/ubuntu-lts

vmType: vz
mountType: virtiofs
cpus: 6
memory: "12GiB"
disk: "80GiB"

containerd:
  system: false
  user: false

# ホストのファイルは一切共有しない。
mounts: []

provision:
  - mode: system
    script: |
      #!/bin/bash
      set -euxo pipefail
      apt-get update
      apt-get install -y ca-certificates curl git build-essential \
        python3 python3-venv python3-pip nodejs npm golang-go \
        uidmap dbus-user-session

      install -m 0755 -d /etc/apt/keyrings
      curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
        -o /etc/apt/keyrings/docker.asc
      chmod a+r /etc/apt/keyrings/docker.asc
      . /etc/os-release
      cat >/etc/apt/sources.list.d/docker.sources <<EOF
      Types: deb
      URIs: https://download.docker.com/linux/ubuntu
      Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}
      Components: stable
      Architectures: $(dpkg --print-architecture)
      Signed-By: /etc/apt/keyrings/docker.asc
      EOF

      apt-get update
      apt-get install -y docker-ce docker-ce-cli containerd.io \
        docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras

      systemctl disable --now docker.service docker.socket containerd.service containerd.socket || true
      systemctl mask docker.service docker.socket containerd.service containerd.socket || true
      loginctl enable-linger "{{.User}}"

  - mode: user
    script: |
      #!/bin/bash
      set -euxo pipefail
      systemctl --user start dbus || true
      dockerd-rootless-setuptool.sh install
      systemctl --user enable --now docker.service
      docker context use rootless

設定では、rootful Docker の system service を明示的に停止・mask しています。mask は、service が誤って起動しないよう systemd から無効化する設定です。その後、通常ユーザーの systemd service として rootless Docker daemon を起動します。rootless Docker では、Docker daemon とコンテナが user namespace 内で動作するため、VM 内でも権限の範囲を少し狭められます。

loginctl enable-linger を指定しているのは、VM のログイン session が閉じても user service を継続させるためです。これにより、shell を抜けた後も rootless Docker daemon を使えます。

検証結果

構築後、VM が次の状態で起動していることを確認しました。

NAME       STATUS   VMTYPE   ARCH      CPUS   MEMORY   DISK
agent-dev  Running  vz       aarch64   6      12GiB    80GiB

続いて、rootless mode、Compose plugin、実際のコンテナ起動を順番に確認します。

limactl shell agent-dev -- docker info --format '{{json .SecurityOptions}}'
# ["name=seccomp,profile=builtin","name=rootless","name=cgroupns"]

limactl shell agent-dev -- docker compose version
# Docker Compose version v5.3.1

limactl shell agent-dev -- docker run --rm hello-world
# Hello from Docker!

name=rootless が表示され、hello-world の arm64 image を VM 内で pull・実行できました。この確認では、ホストの Docker daemon には接続していません。

mount の設計

もっとも安全な方法は、プロジェクトを VM 内で clone することです。ホスト側のファイルを共有しなければ、agent が変更できるファイルは VM 内に限られます。

limactl shell agent-dev
mkdir -p ~/src
cd ~/src
git clone git@github.com:ORG/REPO.git

エディタなどの都合でホスト上の作業ツリーが必要な場合だけ、専用ディレクトリを明示的に共有してください。

mounts:
  - location: "/Users/your-name/agent-workspaces"
    mountPoint: "/mnt/host-workspaces"
    writable: true

この mount を設定すると、agent はそのディレクトリ配下を読み書き・削除できます。そのため、通常のホームディレクトリや既存の全 workspace は共有しないでください。共有用ディレクトリには、使い捨ての clone または Git worktree だけを置きます。.ssh、API key、.env、credential cache、symlink は含めないでください。

依存キャッシュや DB の永続データは、Docker named volume や VM 内の ~/.cache に置くことをおすすめします。host mount 上に node_modules やコンテナ volume を作る必要はありません。

Docker Compose を扱う際の注意

Compose ファイルは、単なる設定データではありません。privilegednetwork_mode: host、volume、build context などを通じて、VM 内で強い操作を要求できます。agent が未知の repository の Compose を起動する場合は、Compose ファイルもコードとして扱い、レビュー対象にしてください。

少なくとも、次の設定は避けてください。

  • privileged: true
  • VM の //run を bind mount する設定
  • Docker socket の container への mount
  • network_mode: host が不要なサービス
  • host path を広範囲に指定した volume

rootless Docker は追加の防御層になりますが、すべての Compose 構成と互換ではありません。特権 container、1024 未満の port、host network などを必要とするプロジェクトでは制約に当たることがあります。その場合も、すぐに rootful Docker へ切り替えるのではなく、本当に必要な機能を先に切り分けてください。

認証情報は後から VM 専用で渡す

この段階では、Codex、Claude Code、GitHub の認証を設定していません。agent が認証情報を利用できる以上、VM が侵害された場合にはその認証情報も流出したものとして扱う必要があります。そのため、ホストで普段使っている強い credential を転用しないでください。

次の段階では、次のような VM 専用かつ用途を絞った認証方法を採用します。

  • Codex: VM 内で codex login を実行します。API key を使う場合は、専用 OpenAI Project と予算上限を設定します。
  • Claude Code: VM 内で browser login を行うか、専用 API key を使います。自動処理では、短命 token を返す apiKeyHelper を検討します。
  • GitHub の read-only clone: VM 内で生成した、repository ごとの deploy key を使います。
  • GitHub の push / PR: 対象 repository と最小権限に絞った短期 fine-grained PAT、または GitHub App installation token を使います。

ホストの ~/.ssh を mount したり、SSH agent forwarding を有効にしたりしないでください。鍵本体が VM に残らなくても、forward された agent はホストアカウントの権限で署名できます。そのため、隔離の目的に合いません。

残るリスク

この VM は、macOS host からの隔離を強めるための実用的な境界です。ただし、悪意のあるコードを完全に安全に実行できる sandbox ではありません。VM 内に置いた認証情報や、VM から許可されているネットワーク通信は、agent から利用できる点に注意してください。

特に、次の二点に注意してください。

  1. Lima の既定 user-mode network では、VM から host.lima.internal 経由で macOS host の loopback に到達できます。ホストの localhost で管理 API や token を使う開発サービスを公開している場合は、host firewall も見直してください。
  2. VM にインターネット egress がある限り、agent が読めるソースや VM 内の credential を外部へ送ることは技術的に可能です。機密性が高い環境では、agent が変更できない egress proxy や allowlist を別途用意してください。

まとめ

最初の一歩として、host mount と credential を持たない Lima VM を作り、VM 内の rootless Docker が動くことを確認してください。ここまでできれば、既存の Docker Compose 開発環境を大きく変えずに、Codex / Claude Code の実行場所を macOS host から切り離せます。

その後、必要最小限の workspace mount、VM 専用 credential、GitHub 権限を順に追加してください。最初から利便性を優先してホームディレクトリやホスト Docker socket を渡さないことが、この構成の重要なポイントです。

参考

DjangoをCloud Runにデプロイ

DjangoをCloud Runにデプロイした時のメモ。

以下の記事を参考にしました。

Cloud Run での Django  |  Google Codelabs

今回はSQLiteを使用しているためCloud SQLは除外しています。
また、SQLiteは別で設定予定のため今回の作業には含まれません。

Cloud API を有効化

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  secretmanager.googleapis.com \
  artifactregistry.googleapis.com

環境変数を設定

export PROJECT_ID=$(gcloud config get-value core/project)
export REGION=us-central1

サービスアカウントを作成

gcloud iam service-accounts create cloudrun-sa
export SERVICE_ACCOUNT=$(gcloud iam service-accounts list \
    --filter cloudrun-sa --format "value(email)")

Artifact Registry を作成

export ARTIFACT_REGISTRY={$REGION}-docker.pkg.dev/{$PROJECT_ID}/containers

Storage バケットを作成

export GS_BUCKET_NAME={$PROJECT_ID}-media
gcloud storage buckets create gs://{$GS_BUCKET_NAME} --location {$REGION} 

バケットを管理する権限をサービスアカウントに付与する。

gcloud storage buckets add-iam-policy-binding gs://{$GS_BUCKET_NAME} \
    --member serviceAccount:{$SERVICE_ACCOUNT} \
    --role roles/storage.admin

Secret Managerに保存

echo GS_BUCKET_NAME=\"{$GS_BUCKET_NAME}\" >> .env
echo SECRET_KEY=\"$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 50 | head -n 1)\" >> .env
echo DEBUG=True >> .env
gcloud secrets create application_settings --data-file .env

サービス アカウントにこのシークレットへのアクセスを許可する。

gcloud secrets add-iam-policy-binding application_settings \
  --member serviceAccount:{$SERVICE_ACCOUNT} --role roles/secretmanager.secretAccessor

作成されたことを確認。

gcloud secrets versions list application_settings
rm .env

設定ファイルを作成

記事を参考に設定ファイルを作成します。

mv myproject/settings.py myproject/basesettings.py
touch myproject/settings.py

myproject/settings.py

import io
import os
from urllib.parse import urlparse

import environ

# Import the original settings from each template
from .basesettings import *

# Load the settings from the environment variable
env = environ.Env()
env.read_env(io.StringIO(os.environ.get("APPLICATION_SETTINGS", None)))

# Setting this value from django-environ
SECRET_KEY = env("SECRET_KEY")

# Ensure myproject is added to the installed applications
if "myproject" not in INSTALLED_APPS:
    INSTALLED_APPS.append("myproject")

# If defined, add service URLs to Django security settings
CLOUDRUN_SERVICE_URLS = env("CLOUDRUN_SERVICE_URLS", default=None)
if CLOUDRUN_SERVICE_URLS:
    CSRF_TRUSTED_ORIGINS = env("CLOUDRUN_SERVICE_URLS").split(",")
    # Remove the scheme from URLs for ALLOWED_HOSTS
    ALLOWED_HOSTS = [urlparse(url).netloc for url in CSRF_TRUSTED_ORIGINS]
else:
    ALLOWED_HOSTS = ["*"]

# Default false. True allows default landing pages to be visible
DEBUG = env("DEBUG", default=False)

# Set this value from django-environ
DATABASES = {"default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3")}

# Define static storage via django-storages[google]
GS_BUCKET_NAME = env("GS_BUCKET_NAME")
STATICFILES_DIRS = []
GS_DEFAULT_ACL = "publicRead"
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
    },
    "staticfiles": {
        "BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
    },
}

requirements.txt(追加)

gunicorn
django-storages[google]
django-environ

Procfileを作成

touch Procfile

Procfile

web: gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 myproject.wsgi:application

イメージをビルドする

gcloud builds submit --pack image={$ARTIFACT_REGISTRY}/myimage

イメージ名は適宜変更します。

Cloud Runにデプロイ

gcloud run deploy django-cloudrun \
  --region $REGION \
  --image {$ARTIFACT_REGISTRY}/myimage \
  --set-secrets APPLICATION_SETTINGS=application_settings:latest \
  --service-account $SERVICE_ACCOUNT \
  --allow-unauthenticated

デプロイが完了して表示されたURLを選択するとDjangoのスタートページが表示されます。

変更の適用

変更があった場合は、以下の操作を行います。

  1. 新しいイメージをビルド
  2. Cloud Run を更新
gcloud builds submit --pack image=${ARTIFACT_REGISTRY}/myimage
gcloud run services update django-cloudrun \
  --region $REGION \
  --image ${ARTIFACT_REGISTRY}/myimage

.envを読み込み

ローカル環境で開発する時は、環境変数.envから読み込めるほうが都合がいい場合もあったのでmyproject/settings.pyに以下を追加しました。

from pathlib import Path

# プロジェクトのルートディレクトリを取得
BASE_DIR = Path(__file__).resolve().parent.parent

# .envファイルを読み込む
env_path = os.path.join(BASE_DIR, '.env')
if Path(env_path).is_file():
  env.read_env(env_path)

YouTubeの動画から字幕テキストを取得する

前回の続きでYouTubeの動画から字幕テキストを取得するときのメモ。

tsyknsr.hatenablog.com

import csv
from youtube_transcript_api import YouTubeTranscriptApi

def get_video_transcript(video_url):
    # 動画IDをURLから抽出
    video_id = video_url.split('v=')[-1].split('&')[0]
    
    try:
        # 字幕を取得
        transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['ja'])
        # 字幕テキストを結合
        transcript_text = ' '.join([entry['text'] for entry in transcript])
        return transcript_text
    except Exception as e:
        print(f"Error retrieving transcript: {e}")
        return None
    
def main():   
    # video_list.csvからURLを読み込み中
    video_transcripts = []
    with open('video_list.csv', mode='r', encoding='utf-8') as file:
        reader = csv.reader(file)
        next(reader)  # ヘッダをスキップ
        for row in reader:
            print(f"Title: {row[0]}")
            video_url = row[1]
            transcript_text = get_video_transcript(video_url)
            video_transcripts.append({'title': row[0], 'url': video_url, 'transcript': transcript_text})

    # 取得した字幕テキストをCSVに出力
    with open('video_transcripts.csv', mode='w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerow(['title', 'url', 'transcript'])
        for video_transcript in video_transcripts:
            writer.writerow([video_transcript['title'], video_transcript['url'], video_transcript['transcript']])

if __name__ == "__main__":
    main()

YouTubeのチャンネルから動画のタイトルとURLの一覧を取得する

YouTubeAPIを使用して動画のタイトルとURLの一覧を取得するプログラムを作成した時のメモ。

import csv
from googleapiclient.discovery import build

# YouTube Data API キーを設定
API_KEY = ''

# チャンネルURLを設定
CHANNEL_URL=''

def get_channel_videos(channel_url):
    # YouTube APIクライアントを作成
    youtube = build('youtube', 'v3', developerKey=API_KEY)
    
    # チャンネルIDを取得
    channel_id = channel_url.split('/')[-1]
    
    # チャンネルの動画を取得
    videos = []
    next_page_token = None
    
    while True:
        request = youtube.search().list(
            part='snippet',
            channelId=channel_id,
            maxResults=50,
            type='video',
            pageToken=next_page_token
        )
        response = request.execute()
        
        for item in response['items']:
            video_title = item['snippet']['title']
            video_url = f"https://www.youtube.com/watch?v={item['id']['videoId']}"
            videos.append({'title': video_title, 'url': video_url})
        
        next_page_token = response.get('nextPageToken')
        if not next_page_token:
            break
    
    return videos

def main():
    video_list = get_channel_videos(CHANNEL_URL)

    with open('video_list.csv', mode='w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerow(['title', 'url'])
        for video in video_list:
            writer.writerow([video['title'], video['url']])

if __name__ == "__main__":
    main()

2023年買ってよかったもの

iPhone15 Pro

毎年値段が上がるので周囲ではAndroidに転向する人も出てきましたが今年も頑張って購入しました。

Ankerのモバイルバッテリー

サイズ感がちょうど良いのとケーブルを持ち歩く必要がないので最近の普段使いはこれに落ち着きました。

aarkeの炭酸水メーカー

こういったオシャレ家電は使わなくなることのほうが多いのですが、自宅で炭酸水をつくれることが意外と便利で日常的に使う家電の仲間入りをしました。

ダンベル 10kg

体力維持レベルの運動が目的なので10kgでも充分でした。しっかりしたダンベルは数万円するので購入に勇気がいりますが、こちらは1万円台で購入しやすい価格帯ということも満足度につながりました。ダンベルが良かったので調子に乗ってベンチも買いましたが、残念ながら埃をかぶりはじめています。

耐熱ボウル

耐熱ボウルの5点セットです。最初は欲しいサイズの1点だけを予定していましたが、実際に使ってみるとそれぞれのサイズで使い分けたほうが圧倒的に料理しやすいのでセットを購入して良かったと思います。

マキネッタ

夏場にアイスカフェラテをつくるのに大活躍しました。手入れも大雑把でいいので性分に合っていました。豆を自分で挽くか悩みましたが、面倒になり使用する機会が減ると思ったので、豆を購入した時にお店で挽いてもらいました。

RICOH GR IIIx

サイズ感と重さと軽さが自分の生活に合っていました。iPhone15 Pro購入以降は持ち出す回数が減りましたが、旅行に行った時などにGRを使用しています。数年前に思い切って購入したミラーレス一眼は、今年は使用する機会がなかったので来年売却するか悩んでいます。

漫画

最近読んで面白い作品でした。

PS5

年末年始のお供にホグワーツ・レガシーと合わせて購入しました。ゲームはエンタメの最先端のひとつであることを学びました。