[{"data":1,"prerenderedAt":551},["ShallowReactive",2],{"post-index":3,"post-worktree-shared-reverse-proxy":453},{"posts":4,"categories":402,"tags":411},[5,17,35,43,60,74,87,105,122,144,156,167,179,193,208,223,239,250,264,275,285,296,308,319,331,343,356,367,377,387],{"slug":6,"title":7,"description":8,"publishedAt":9,"updatedAt":9,"category":10,"tags":13,"thumbnail":16},"ai-cognitive-debt-approval"," 認知負債は「承認」で生まれる ── コーディングエージェント時代の意図負債と、個人・チームの向き合い方","コーディングエージェントに実装を任せると、コードは速く書けるのに人の理解が追いつかない。認知負債・意図負債・理解負債の違いを「下り」と「上り」で整理し、負債が生まれる承認の瞬間と、個人とチームでできる対策、まだ答えのない問いを、自分の体験から考える。","2026-11-03T17:12:07.686Z",{"name":11,"slug":12},"AI","ai",[14],{"name":15,"slug":15},"organization",null,{"slug":18,"title":19,"description":20,"publishedAt":21,"updatedAt":21,"category":22,"tags":23,"thumbnail":16},"design-harness-personal-product","デザインハーネスを個人開発で組んだ — AI が作る画面のずれを止めるまで","デザイナーのいない個人開発で、AI が作る画面のずれを止めるまでの記録。Codex の Product Design で画像の案を出して画面を作り直し、デザインファイルとコードの食い違いを正本の分担で片付け、lint・見た目の比較・AI の評価役からなるデザインハーネスで外れたら止める仕組みを組んだ。","2026-11-03T15:39:02.665Z",{"name":11,"slug":12},[24,27,30,32],{"name":25,"slug":26},"claude code","claude-code",{"name":28,"slug":29},"design system","design-system",{"name":31,"slug":31},"codex",{"name":33,"slug":34},"design harnes","design-harnes",{"slug":36,"title":37,"description":38,"publishedAt":39,"updatedAt":39,"category":40,"tags":41,"thumbnail":16},"claude-code-mods-intro","Claude Codeの「Claude Mods」とは？ 入れてみた3つのmodと、安全に入れる手順","Claude Codeの新機能Claude Mods（v2.1.287〜）を紹介する。hooks・skills・MCPとの違い、入れてみたmod 3つ（prompt-rail・md-prompt・qa-guide）の使い方、claude plugin validateで中身を確かめて入れる手順をまとめた。","2026-11-03T15:30:44.678Z",{"name":11,"slug":12},[42],{"name":25,"slug":26},{"slug":44,"title":45,"description":46,"publishedAt":47,"updatedAt":47,"category":48,"tags":51,"thumbnail":16},"worktree-shared-reverse-proxy","AI 時代の並列開発の環境構築 — git worktree ごとにアプリを同時に動かす","git worktree の並列開発で、各 worktree のフロントとバックを同時に動かす Docker Compose 構成を紹介する。ポートはずらさず共有のリバースプロキシでホスト名に振り分け、DB は 1 つを共有ネットワークに置いて全 worktree から参照する。","2026-11-03T15:24:32.067Z",{"name":49,"slug":50},"Dev Env","dev-env",[52,54,57],{"name":53,"slug":53},"git",{"name":55,"slug":56},"Docker","docker",{"name":58,"slug":59},"開発環境","development-environment",{"slug":61,"title":62,"description":63,"publishedAt":64,"updatedAt":64,"category":65,"tags":66,"thumbnail":16},"ghostty-herdr-dev-env","Ghostty + herdr で組む開発環境（2026年9月版） — AI エージェントを並べてターミナルで開発する","WezTerm + tmux + 自作ツールから Ghostty + herdr に移した私の開発環境を紹介する。Claude Code・Codex・takt を並べて動かす 1 タスクの流れ、ターミナル内ブラウザで画面の要素を AI に渡す方法、Neovim・lazygit・hunk・Nix まで層ごとにまとめた。","2026-11-03T15:14:47.700Z",{"name":49,"slug":50},[67,68,70,72],{"name":58,"slug":59},{"name":69,"slug":69},"herdr",{"name":71,"slug":71},"ghostty",{"name":73,"slug":73},"terminal",{"slug":75,"title":76,"description":77,"publishedAt":78,"updatedAt":78,"category":79,"tags":80,"thumbnail":16},"dotfiles-operation-and-nix","設定ファイルを端末に置いたままにしない — dotfiles 運用のすすめと、その先の Nix","設定ファイルを Git リポジトリで管理する dotfiles 運用の始め方と、その先で効いてくる Nix の話。何を入れて何を入れないか、配線の 3 手法の比較、運用して見えてくる穴、3 端末を 1 つの設定で回すところまでを実例で書く。","2026-11-03T15:04:48.084Z",{"name":49,"slug":50},[81,82,85],{"name":58,"slug":59},{"name":83,"slug":84},"Nix","nix",{"name":86,"slug":86},"dotfiles",{"slug":88,"title":89,"description":90,"publishedAt":91,"updatedAt":91,"category":92,"tags":93,"thumbnail":102},"opencode-go-plan-evaluation","OpenCode Go プランを評価する — 月$10 で LLM オーケストレーションの中間層は作れるか","OpenCode が提供する月額固定の Go プラン（初月$5、翌月以降$10\u002F月）を、料金体系・想定ユースケース・3層 LLM 構成における中間モデル層としての適合性まで含めて評価する。","2026-05-07T12:12:31.936Z",{"name":11,"slug":12},[94,97,100],{"name":95,"slug":96},"Harness Engineering","harness-engineering",{"name":98,"slug":99},"LLM","llm",{"name":101,"slug":101},"opencode",{"url":103,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F6Nh8tVuSCuHunaVeqyegdU\u002F0fee3536b9ee406b986430ebc1b8ebfd\u002FOpenCode_Go_%C3%A3__%C3%A3__%C3%A3__%C3%A3__%C3%A3__%C3%A8__%C3%A4__%C3%A3__%C3%A3__.png","",{"slug":106,"title":107,"description":108,"publishedAt":109,"updatedAt":109,"category":110,"tags":111,"thumbnail":120},"prompt-context-harness-engineering","ハーネスエンジニアリングとは何か：プロンプト→コンテキスト→ハーネスへ至るAIエージェント設計の変遷","「ハーネスエンジニアリング」とは何か。プロンプトエンジニアリング、コンテキストエンジニアリングから続く3段階の変遷を、論文引用（Lost in the Middle \u002F SWE-bench）とClaude Codeの実装例（フック・ルール・Skill）で階層構造として整理する。","2026-04-07T13:04:49.564Z",{"name":11,"slug":12},[112,115,116,119],{"name":113,"slug":114},"Prompt Engineering","prompt-engineering",{"name":95,"slug":96},{"name":117,"slug":118},"Context Engineering","context-engineering",{"name":98,"slug":99},{"url":121,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F74HnVGS2R8wphJ1nGHgLfb\u002Fb87d3d6e35ccc518e04d9959c7637a88\u002Fã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã_ªã__ã__ã__ã__ã__ä__ã__ï__ã__ã__ã__ã__ã__ã__ã__â__ã__ã__ã__ã__ã__ã__â__ã__ã__ã__ã__ã__è__ã__A.png",{"slug":123,"title":124,"description":125,"publishedAt":126,"updatedAt":126,"category":127,"tags":130,"thumbnail":142},"authjs-v5-jwt-dual-timeout-bff-django","Auth.js v5 でJWT二重タイムアウトを実装する：BFF + Django連携の設計パターン","Auth.js v5のjwt callbackで8時間の絶対有効期限と30分のアイドルタイムアウトを両立する実装方法を解説。Next.js MiddlewareによるBFF API保護とDjango REST Frameworkとの Token Exchange パターンも紹介する。","2026-04-06T12:45:28.328Z",{"name":128,"slug":129},"Programming","programming",[131,134,136,139],{"name":132,"slug":133},"auth.js","auth-js",{"name":135,"slug":135},"jwt",{"name":137,"slug":138},"Django REST Framework","django-rest-framework",{"name":140,"slug":141},"Next.js","next-js",{"url":143,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002FpqkFfHKpf2UqoNrB6KrKS\u002F672a78076f28d88b31509c230f9c3f64\u002FAuth.js_v5_ã__ã__JWTäº_é__ã__ã__ã__ã__ã__ã__ã__å__è__ã__ã__ï__BFF___Djangoé__æ_ºã__è__è__ã__ã__ã__ã__ã__.png",{"slug":145,"title":146,"description":147,"publishedAt":148,"updatedAt":148,"category":149,"tags":150,"thumbnail":154},"claude-code-spec-to-growing-claudemd","仕様書を「渡す」からCLAUDE.mdを「育てる」へ — Claude Codeの使い方が変わった話","Claude Codeに仕様書を丸ごと渡す運用から、CLAUDE.mdを継続的に育てる運用へ。「間違えたら追加」ルールの実践例と、20行→146行+31スキルに育てた記録。","2026-04-02T14:58:11.375Z",{"name":11,"slug":12},[151,152,153],{"name":25,"slug":26},{"name":113,"slug":114},{"name":117,"slug":118},{"url":155,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002FqybhQ2TQSYQhIvDhWxqvs\u002F9f8b888e11a76223c8a1b1da42df5ce1\u002Fä__æ__æ__ã__ã__æ__ã__ã__ã__ã__CLAUDE.mdã__ã__è__ã__ã__ã__ã___â___Claude_Codeã__ä__ã__æ__ã__ã__å__ã__ã__ã__è__.png",{"slug":157,"title":158,"description":159,"publishedAt":160,"updatedAt":160,"category":161,"tags":162,"thumbnail":165},"claude-code-harness-engineering","Claude Codeを覚醒させる「ハーネスエンジニアリング」 CLAUDE.md・Hooks・Rules設定の徹底解説","プロンプトを書く時代から、AIが最高に働く環境を設計する時代へ。Claude CodeのCLAUDE.md、Hooks、Rules、Settings、Agent Routingを体系的に解説し、自作のオーケストレーションシステム「ai-orchestra」による統合管理の実践例を紹介する。","2026-03-22T08:59:48.575Z",{"name":11,"slug":12},[163,164],{"name":25,"slug":26},{"name":95,"slug":96},{"url":166,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F3liirUgzBlFBuAMoytyN5E\u002F66ba6b07385791bcaae4ddf404f06ce1\u002FClaude_Codeã__è__é__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã_ªã__ã__ã__ã___CLAUDE.mdã__Hooksã__Rulesè__å__ã__å__åº_è__èª_.png",{"slug":168,"title":169,"description":170,"publishedAt":171,"updatedAt":172,"category":173,"tags":174,"thumbnail":177},"claude-code-dev-toolchain","Claude Code開発を支える私のツールチェイン — ターミナルからAIデザインまで","Claude Code CLIを中心とした開発環境を紹介する。WezTerm + Tmuxのターミナル構成、GTRやBatonによる並列開発、ステータスライン・通知フックのカスタマイズ、MCP\u002Fプラグイン活用、Aqua VoiceやPencilとの連携まで、実際に使っているツールチェインの全体像をまとめた。","2026-03-21T19:30:43.616Z","2026-11-03T15:18:14.982Z",{"name":49,"slug":50},[175,176],{"name":25,"slug":26},{"name":58,"slug":59},{"url":178,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F4wFUBAqC9yuIgeo3xWVHJM\u002F8843248b854fee93872b5ac519d16bc2\u002FClaude_Codeé__ç_ºã__æ__ã__ã__ç__ã__ã__ã__ã__ã__ã__ã__ã___â___ã__ã__ã__ã__ã__ã__ã__AIã__ã__ã_µã__ã__ã__ã__ã__ã__.png",{"slug":180,"title":181,"description":182,"publishedAt":183,"updatedAt":184,"category":185,"tags":186,"thumbnail":191},"lazygit-commit-message-auto-generate","【コピペで導入】lazygit × Claude Code でコミットメッセージ自動生成","lazygit の customCommands と Claude Code の --print モードを組み合わせ、Ctrl+G 一発で Conventional Commits 形式のコミットメッセージを自動生成する方法を解説。コピペ可能な完成形設定と、4段階の試行錯誤で得たハマりポイントを共有します。","2026-03-15T04:36:20.532Z","2026-11-03T15:18:53.496Z",{"name":49,"slug":50},[187,188,189],{"name":53,"slug":53},{"name":25,"slug":26},{"name":190,"slug":190},"lazygit",{"url":192,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F62ov1mzeLUjRm6cbzQ7R92\u002F3bc21a7fe7ec517ad3258b6264d4097e\u002Fã__ã__ã__ã__ã__ã__ã__ã__å__å__ã__lazygit_Ã__Claude_Code_ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__ã__è_ªå__ç__æ__.png",{"slug":194,"title":195,"description":196,"publishedAt":197,"updatedAt":198,"category":199,"tags":200,"thumbnail":206},"git-worktree-runner-guide","git-worktree-runner で worktree 管理が劇的にラクになった話","git-worktree-runner（GTR）は git worktree の操作を 1 コマンドに集約する CLI ラッパーです。Taskfile.yaml での運用から GTR に移行した体験を元に、インストール・設定・AI 並列開発での活用法を解説します。","2026-03-09T15:32:31.316Z","2026-11-03T15:18:33.924Z",{"name":49,"slug":50},[201,202,204],{"name":53,"slug":53},{"name":203,"slug":203},"git-worktree",{"name":205,"slug":205},"git-worktree-runner",{"url":207,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F7BRBfHFocF2ABOeOQmtIic\u002Fa63ce99493098bc42123e4315f43d768\u002Fgit-worktree-runner_ã__ã___worktree_ç__ç__ã__ã__å__ç__ã__ã__ã__ã__ã_ªã__ã__è__.png",{"slug":209,"title":210,"description":211,"publishedAt":212,"updatedAt":212,"category":213,"tags":214,"thumbnail":221},"rag-basic-knowledge","RAG入門 — 検索拡張生成の基礎知識を実験結果とともに学ぶ","RAG（検索拡張生成）の仕組みをVectorDB・Embedding・チャンク分割の3要素に分解して解説。Embeddingモデル変更で正答率0\u002F5→4\u002F5に改善した検証データも公開。","2026-03-01T11:29:25.841Z",{"name":11,"slug":12},[215,218,219],{"name":216,"slug":217},"RAG","rag",{"name":98,"slug":99},{"name":220,"slug":220},"ollama",{"url":222,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F1enfpUixTuvis0oX9x5Cfa\u002F934aed76ac994412ceb37dfdf5dc6f6c\u002FRAGå__é___â___æ__ç__æ__å_µç__æ__ã__å_ºç__ç__è__ã__å__é__çµ_æ__ã__ã__ã__ã__å__ã_µã__.png",{"slug":224,"title":225,"description":226,"publishedAt":227,"updatedAt":228,"category":229,"tags":230,"thumbnail":237},"engineer-ai-daily-workflow","1人のエンジニアがAIを日常にどう組み込んでいるか — 契約サービスから活用フローまで全公開","Claude Code、ChatGPT、Gemini、Aqua Voice、Adobe Fireflyの5サービスを使い分けるエンジニアが、開発・情報収集・ブレスト・音声入力・振り返りまで、AIを日常に組み込んだリアルな活用フローを全公開します。","2026-02-18T14:13:58.531Z","2026-02-18T14:14:12.514Z",{"name":11,"slug":12},[231,232,234],{"name":98,"slug":99},{"name":233,"slug":233},"openclaw",{"name":235,"slug":236},"マルチエージェント","multi-agent",{"url":238,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F5gBC06O8WX2GX8wmoOXQfy\u002F27a11e0f280f642a781e462dead9d3ae\u002F1äººã__ã__ã__ã__ã__ã__ã__ã__ã__AIã__æ__å__ã__ã__ã__ã__çµ_ã__è__ã__ã__ã__ã__ã__ã___â___å__ç__ã_µã__ã__ã__ã__ã__ã__æ__ç__ã__ã_.png",{"slug":240,"title":241,"description":242,"publishedAt":243,"updatedAt":243,"category":244,"tags":245,"thumbnail":248},"ai-orchestra-design-and-practice","Claude Codeマルチエージェント実践｜AI Orchestraで超えた2つの壁","Claude Code単体運用で生じるコンテキスト溢れ・役割混乱の課題を、25種の専門エージェントとClaude×Codex×Geminiのマルチモデル連携で解決するオーケストレーションシステム「AI Orchestra」。その設計思想、Hookによる自動ルーティング、並列レビューなどの実装を具体的に紹介します。","2026-02-08T08:01:16.212Z",{"name":11,"slug":12},[246,247],{"name":235,"slug":236},{"name":98,"slug":99},{"url":249,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F6j2KWOOo7wJfUobTCg3o2w\u002F126c8d413cd9a56e64fba107544ec4e1\u002FClaude_Codeã__ã__ã__ã__ã__ã__ã__ã__ã__ã__å__è_µï__AI_Orchestraã__ã__è__ã__ã__2ã__ã__å__.png",{"slug":251,"title":252,"description":253,"publishedAt":254,"updatedAt":255,"category":256,"tags":257,"thumbnail":262},"ai-benchmark-guide","AIベンチマーク読み方ガイド｜SWE-bench・GPQA・ARC-AGIの意味と活用法","SWE-bench、GPQA Diamond、ARC-AGI、Terminal-Benchなど主要AIベンチマークの読み方を解説。2026年2月最新のモデルスコア比較と、コーディングエージェントへのタスク振り分けに活かす実践的な指針を紹介します。","2026-02-07T16:10:19.131Z","2026-02-07T16:11:56.095Z",{"name":11,"slug":12},[258,259],{"name":98,"slug":99},{"name":260,"slug":261},"Benchmark","benchmark",{"url":263,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F3QlfPsXmLogdj9o4mdgFYQ\u002F73a69ca272ac4e4fdcf977f322c0218a\u002FAIã__ã__ã__ã__ã__ã__ã__èª_ã__æ__ã__ã__ã__ã__ã__ï__SWE-benchã__GPQAã__ARC-AGIã__æ__å__ã__æ__ç__æ__.png",{"slug":265,"title":266,"description":267,"publishedAt":268,"updatedAt":268,"category":269,"tags":270,"thumbnail":273},"multi-agent-building-guide","マルチエージェント構築入門｜構成の全体像と3つの実装アプローチ","マルチエージェントの構築方法を解説。オーケストレーター・サブエージェント・ツール連携の3要素から、自作・フレームワーク・既存ツール活用の選択肢まで。Claude Codeでの実装例付き。","2026-02-05T12:52:58.095Z",{"name":11,"slug":12},[271,272],{"name":98,"slug":99},{"name":235,"slug":236},{"url":274,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F68XZFvLA17pegNy0lHCrYN\u002Ff90202a483bf087f4fa4cbb80dae934e\u002Fã__ã__ã__ã__ã__ã__ã__ã__ã__ã__æ__ç__å__é__ï__æ__æ__ã__å__ä__å__ã__3ã__ã__å__è__ã__ã__ã__ã__ã__ã__.png",{"slug":276,"title":277,"description":278,"publishedAt":279,"updatedAt":279,"category":280,"tags":281,"thumbnail":283},"openclaw-security-and-features","OpenClaw徹底解説 ── セルフホスト型AIアシスタントの可能性とセキュリティリスク","「AIが自分の代わりにメッセージを返信してくれたら…」そんな願いを叶えるツールが、いま爆発的な人気を集めています。その名も「OpenClaw」。わずか2ヶ月でGitHub Stars 10万を突破し、週間200万人が訪れるほどの注目を浴びています。  しかし、その急成長の裏には商標問題、詐欺騒動、そして深刻なセキュリティ脆弱性の発覚と、波乱万丈なドラマがありました。","2026-02-03T15:20:26.415Z",{"name":11,"slug":12},[282],{"name":233,"slug":233},{"url":284,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F7aKouVOZSGnzVUGgLQfa22\u002F5688eb515dc5f0b2c301b9b8c267ec20\u002F00036.png",{"slug":286,"title":287,"description":104,"publishedAt":288,"updatedAt":289,"category":290,"tags":291,"thumbnail":294},"custom-instructions","ChatGPTやClaudeがもっと賢くなる！カスタム指示の書き方と実例集","2026-01-30T17:42:57.415Z","2026-02-03T15:03:06.461Z",{"name":11,"slug":12},[292,293],{"name":98,"slug":99},{"name":113,"slug":114},{"url":295,"description":287},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F4EojonTOebJGeiaXf98ol0\u002F9e391fc8bd6e548f8c96136c805b49d7\u002FChatGPTã__Claudeã__ã__ã__ã__ã__è__ã__ã_ªã__ï__ã__ã__ã__ã__æ__ç_ºã__æ__ã__æ__ã__å__ä__é__.png",{"slug":297,"title":298,"description":299,"publishedAt":300,"updatedAt":301,"category":302,"tags":303,"thumbnail":306},"ollama-local-llm-setup","ローカルLLMはまず小さめモデルから：Ollamaで始める導入〜疎通確認","acでOllamaを使い、ローカルLLMを最短で起動→1回応答まで確認する手順を解説。モデル選定の考え方、遅い\u002Fメモリ\u002Fディスクの切り分け、API疎通(curl)もまとめます。","2026-01-18T06:40:49.270Z","2026-01-21T13:05:14.924Z",{"name":11,"slug":12},[304,305],{"name":220,"slug":220},{"name":98,"slug":99},{"url":307,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F1NQmtspOD1S6LEb4SrCYIQ\u002F49c213de131a9a4ea139c32e805f3b01\u002Fã__ã__ã__ã__LLMã__ã__ã__ã__å__ã__ã__ã__ã__ã__ã__ã__ã__ï__Ollamaã__ã__å__ã__ã__å__å__ã__ç__é__ç_ºèª_.png",{"slug":309,"title":310,"description":104,"publishedAt":311,"updatedAt":312,"category":313,"tags":314,"thumbnail":317},"llm-leaderboard-score-guide","LLMリーダーボードの読み方：スコア指針とベンチマーク確認手順","2026-01-10T08:57:26.636Z","2026-01-18T06:55:58.990Z",{"name":11,"slug":12},[315,316],{"name":260,"slug":261},{"name":98,"slug":99},{"url":318,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F8u7GQ3mAlcvIHOc5bFJc6\u002Fa953bf74c33117efa393eaf8541f515a\u002FLLMã_ªã__ã__ã__ã__ã__ã__ã__ã__ã__ã__èª_ã__æ__.png",{"slug":320,"title":321,"description":104,"publishedAt":322,"updatedAt":322,"category":323,"tags":324,"thumbnail":329},"vite_docker_esbuild-error","Vite + Docker + ESBuild Errorの解決","2021-11-12T03:53:22.965Z",{"name":128,"slug":129},[325,326],{"name":55,"slug":56},{"name":327,"slug":328},"Vite","vite",{"url":330,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F5gqsl4ZChCI3uaSXQA86du\u002F3c94f9928ffaaab2d8911d32e742de1e\u002Fprogramming.jpg",{"slug":332,"title":333,"description":104,"publishedAt":334,"updatedAt":335,"category":336,"tags":337,"thumbnail":341},"nuxt-router-scroll-control","Nuxt.jsのページ遷移時のスクロール制御したい","2021-10-28T14:31:11.276Z","2021-10-28T14:35:33.155Z",{"name":128,"slug":129},[338],{"name":339,"slug":340},"Nuxt.js","nuxt-js",{"url":342,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F6dqpNPZsBJJ7x9XJj1Ld1z\u002Fcd1bb9323a8ffb0657ad79525403e1ff\u002Fnuxt.jpg",{"slug":344,"title":345,"description":104,"publishedAt":346,"updatedAt":347,"category":348,"tags":349,"thumbnail":354},"react-drf-img_uploader-backend","React × DRFで画像アップロード機能作成 (バックエンド実装編)","2021-10-26T02:56:58.215Z","2021-10-27T11:37:43.829Z",{"name":128,"slug":129},[350,353],{"name":351,"slug":352},"React","react",{"name":137,"slug":138},{"url":355,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F4jbVtK0nXsMKZplpBAwhri\u002Fb0ccef5153d92a7947eb0c682008255c\u002Fdjango-rest-framework.jpg",{"slug":357,"title":358,"description":104,"publishedAt":359,"updatedAt":360,"category":361,"tags":362,"thumbnail":365},"react-drf-img_uploader-frontend","React × DRFで画像アップロード機能作成 (フロント実装編)","2021-10-26T02:39:01.629Z","2021-10-27T11:37:24.670Z",{"name":128,"slug":129},[363,364],{"name":351,"slug":352},{"name":137,"slug":138},{"url":366,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F3uFOs5NyHOvRqYgE1lQ3p6\u002Fd79c8fabb7d4151c5773ffe1cdd2bce3\u002Freact.jpg",{"slug":368,"title":369,"description":104,"publishedAt":370,"updatedAt":371,"category":372,"tags":373,"thumbnail":376},"react-drf-img_uploader-environment","React × DRFで画像アップロード機能作成 (環境構築編)","2021-10-26T02:00:01.702Z","2021-10-27T11:47:28.398Z",{"name":128,"slug":129},[374,375],{"name":351,"slug":352},{"name":137,"slug":138},{"url":330,"description":104},{"slug":378,"title":379,"description":104,"publishedAt":380,"updatedAt":381,"category":382,"tags":383,"thumbnail":385},"docker-install-procedure","各OS毎のDockerインストール手順","2021-10-26T01:43:54.692Z","2022-02-07T14:32:07.791Z",{"name":128,"slug":129},[384],{"name":55,"slug":56},{"url":386,"description":104},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F3aVlE8NIfX12QvTafaC8xE\u002Fd00bf480e212538e1816fc10d2e7b77d\u002Fdocker.jpg",{"slug":388,"title":389,"description":104,"publishedAt":390,"updatedAt":391,"category":392,"tags":395,"thumbnail":399},"notion-clear-trash","Notionのゴミ箱を一括削除する","2021-09-12T13:04:59.853Z","2021-10-21T14:29:03.528Z",{"name":393,"slug":394},"Lifestyle","lifestyle",[396],{"name":397,"slug":398},"Notion","notion",{"url":400,"description":401},"https:\u002F\u002Fimages.ctfassets.net\u002Fd3b1looh8iwg\u002F5binM4YxsZdSXX9SKkGyNP\u002Fcf39353211db8b3e0c4ac18e03b55527\u002Fnotion.jpg","Notion記事用のサムネイル",[403,405,407,409],{"name":49,"slug":50,"count":404},6,{"name":11,"slug":12,"count":406},16,{"name":128,"slug":129,"count":408},7,{"name":393,"slug":394,"count":410},1,[412,414,415,417,418,420,421,422,423,424,425,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450],{"name":98,"slug":99,"count":413},10,{"name":25,"slug":26,"count":404},{"name":137,"slug":138,"count":416},4,{"name":58,"slug":59,"count":416},{"name":55,"slug":56,"count":419},3,{"name":53,"slug":53,"count":419},{"name":95,"slug":96,"count":419},{"name":113,"slug":114,"count":419},{"name":351,"slug":352,"count":419},{"name":235,"slug":236,"count":419},{"name":260,"slug":261,"count":426},2,{"name":117,"slug":118,"count":426},{"name":220,"slug":220,"count":426},{"name":233,"slug":233,"count":426},{"name":132,"slug":133,"count":410},{"name":31,"slug":31,"count":410},{"name":33,"slug":34,"count":410},{"name":28,"slug":29,"count":410},{"name":86,"slug":86,"count":410},{"name":71,"slug":71,"count":410},{"name":203,"slug":203,"count":410},{"name":205,"slug":205,"count":410},{"name":69,"slug":69,"count":410},{"name":135,"slug":135,"count":410},{"name":190,"slug":190,"count":410},{"name":140,"slug":141,"count":410},{"name":83,"slug":84,"count":410},{"name":397,"slug":398,"count":410},{"name":339,"slug":340,"count":410},{"name":101,"slug":101,"count":410},{"name":15,"slug":15,"count":410},{"name":216,"slug":217,"count":410},{"name":73,"slug":73,"count":410},{"name":327,"slug":328,"count":410},{"name":451,"slug":451,"count":452},"takt",0,{"slug":44,"title":45,"description":46,"publishedAt":47,"updatedAt":47,"category":454,"tags":455,"thumbnail":16,"html":459,"headings":460,"readingMinutes":519,"older":520,"newer":527,"related":531},{"name":49,"slug":50},[456,457,458],{"name":53,"slug":53},{"name":55,"slug":56},{"name":58,"slug":59},"\u003Cp>AI エージェントに worktree を 1 つずつ渡して並列で開発していると、「この worktree のブランチで、画面と API がちゃんと動くか見たい」という場面が何度も来る。ところが同じアプリを 2 つ目、3 つ目と起動すると、ポートもコンテナ名も DB もぶつかる。\u003C\u002Fp>\n\u003Cp>私が開発している Web アプリは、Next.js（frontend）、Go（backend）、Python（AI 用の API）、PostgreSQL、ベクトル DB を Docker Compose で動かしている。これまでは worktree を作るときのスクリプトでポートをずらしてしのいでいたが、2026 年 9 月にこれをやめ、共有のリバースプロキシでホスト名に振り分ける構成に変えた。この記事では、その構成を「何を共有し、何を worktree ごとに分けるか」でまとめる。\u003C\u002Fp>\n\u003Cp>先に結論を書く。\u003C\u002Fp>\n\u003Cul>\n\u003Cli>全 worktree で 1 つを共有するもの（入口のリバースプロキシ、PostgreSQL）と、worktree ごとに分けるもの（アプリのコンテナ、依存、ベクトル DB）を分ける\u003C\u002Fli>\n\u003Cli>HTTP のポートは worktree ごとにずらさない。ホストに開くのはプロキシの 3000 番だけにして、\u003Ccode>http:\u002F\u002F&lt;プロジェクト名&gt;.localhost:3000\u003C\u002Fcode> のホスト名で各 worktree に振り分ける\u003C\u002Fli>\n\u003Cli>DB は共有ネットワークに 1 つ置き、どの worktree の backend も同じ接続先 \u003Ccode>db:5432\u003C\u002Fcode> で参照する。ただし、互換性のない migration をブランチごとに並行して試す使い方には向かない\u003C\u002Fli>\n\u003Cli>ポートずらしの頃と比べて、どの方法で作った worktree も同じ手順で起動でき、URL を見ればどの worktree か分かるようになった\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>検証環境は macOS、OrbStack（Docker 29.4、Compose 5.1.2）、Traefik v3.6 で、2026 年 9 月 29 日に確認した。Windows（WSL2）では試していない。\u003C\u002Fp>\n\u003Ch2 id=\"やりたいこと:-各-worktree-のアプリを同時に起動して確かめたい\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">01\u003C\u002Fspan>やりたいこと: 各 worktree のアプリを同時に起動して確かめたい\u003C\u002Fh2>\n\u003Cp>git worktree は、1 つのリポジトリから作業ディレクトリを複数作り、それぞれで別のブランチをチェックアウトしておける git の機能だ。ブランチを切り替えずに並行して作業できるので、AI エージェントを並べて動かすときによく使われる。私は作成と削除に git-worktree-runner（gtr）を使っている（\u003Ca href=\"\u002Fblog\u002Fgit-worktree-runner-guide\">git-worktree-runner の紹介記事\u003C\u002Fa>）。\u003C\u002Fp>\n\u003Cp>コードを書くのはエージェントに任せても、動くかどうかは自分の目で見たい。そこで worktree A と worktree B のアプリを同時に \u003Ccode>docker compose up\u003C\u002Fcode> すると、次のものがぶつかる。\u003C\u002Fp>\n\u003Cdiv class=\"tbl\" tabindex=\"0\" role=\"region\" aria-label=\"Table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>ぶつかるもの\u003C\u002Fth>\n\u003Cth>何が起きるか\u003C\u002Fth>\n\u003Cth>解き方（章）\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>コンテナ名・ネットワーク・volume\u003C\u002Ftd>\n\u003Ctd>後から起動した側が、同じ名前のコンテナを置き換える\u003C\u002Ftd>\n\u003Ctd>worktree ごとに compose のプロジェクト名を分ける（名前）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>ホストのポート\u003C\u002Ftd>\n\u003Ctd>2 つ目の frontend が 3000 番を取れずに起動しない\u003C\u002Ftd>\n\u003Ctd>入口を 1 つにしてホスト名で振り分ける（ポート）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>cookie\u003C\u002Ftd>\n\u003Ctd>ポートをずらしても、同じ \u003Ccode>localhost\u003C\u002Fcode> なので cookie は共有される\u003C\u002Ftd>\n\u003Ctd>worktree ごとにホスト名を変える（ポート）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>DB\u003C\u002Ftd>\n\u003Ctd>worktree ごとに立てるとポートとコンテナ名で衝突し、データも別々になる\u003C\u002Ftd>\n\u003Ctd>1 つを共有ネットワークに置く（DB）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>依存（node_modules）\u003C\u002Ftd>\n\u003Ctd>別の worktree が入れた依存で動いてしまう\u003C\u002Ftd>\n\u003Ctd>worktree ごとの volume に置く（依存と起動）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>cookie はポートを区別しない（\u003Ca href=\"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6265#section-8.5\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6265 §8.5\u003C\u002Fa>）。ポートをずらすだけでは、ある worktree でログインした cookie が別の worktree にも送られる。\u003C\u002Fp>\n\u003Ch2 id=\"結論:-入口と-db-は共有し、アプリは-worktree-ごとに分ける\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">02\u003C\u002Fspan>結論: 入口と DB は共有し、アプリは worktree ごとに分ける\u003C\u002Fh2>\n\u003Cp>全体はこうなる。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fstatic.zenn.studio\u002Fuser-upload\u002F52d3e43126a8-20261001.png\" alt=\"全体構成図。ブラウザは myapp-feat-a.localhost:3000 と myapp-feat-b.localhost:3000 でリバースプロキシにアクセスする。プロキシは全 worktree で共有し、ホストには 127.0.0.1:3000 だけを開く。プロキシは \u002F を各 worktree の frontend に、\u002Fapi\u002Fv1 を backend に転送する。worktree A と B はそれぞれ frontend・backend・ベクトル DB を持ち、両方の backend が全 worktree で共有する PostgreSQL（別名 db）に db:5432 で接続する\" loading=\"lazy\">\u003C\u002Fp>\n\u003Cp>さっきの表と対応させると、共有するものと分けるものは次のとおり。\u003C\u002Fp>\n\u003Cdiv class=\"tbl\" tabindex=\"0\" role=\"region\" aria-label=\"Table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>対象\u003C\u002Fth>\n\u003Cth>全 worktree で 1 つを共有\u003C\u002Fth>\n\u003Cth>worktree ごとに分ける\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>名前\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>compose のプロジェクト名（ホスト名にも使う）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>入口（HTTP のポート）\u003C\u002Ftd>\n\u003Ctd>リバースプロキシ（ホストの 3000 番）\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>DB\u003C\u002Ftd>\n\u003Ctd>PostgreSQL と DB の管理画面\u003C\u002Ftd>\n\u003Ctd>ベクトル DB\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>アプリ\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>frontend \u002F backend \u002F AI 用 API のコンテナ\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>依存\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>node_modules の volume\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>設定\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>.env\u003C\u002Fcode>（同じ中身を使い回す）\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>ここで compose の用語を補っておく。compose のプロジェクトは、\u003Ccode>docker compose\u003C\u002Fcode> が一緒に管理するコンテナ・ネットワーク・volume のまとまりで、プロジェクト名がそれぞれの名前の頭に付く。既定ではディレクトリ名がプロジェクト名になる。ネットワークのほうは、compose で \u003Ccode>external: true\u003C\u002Fcode> と宣言すると、別のプロジェクトが作ったものに参加できる。\u003C\u002Fp>\n\u003Cp>共有するプロキシと DB は、それぞれ別のプロジェクトとして 1 つだけ起動する。各 worktree のプロジェクトは、そのネットワークに参加するだけだ。この記事の構成は、ほぼこれで全部である。\u003C\u002Fp>\n\u003Cp>ただ、ホスト名で振り分けられるのは HTTP だけだ。PostgreSQL の 5432 番とデバッガのポートは固定のまま残る。\u003C\u002Fp>\n\u003Ch2 id=\"まず-worktree-ごとに名前を分ける（compose-のプロジェクト名）\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">03\u003C\u002Fspan>まず worktree ごとに名前を分ける（compose のプロジェクト名）\u003C\u002Fh2>\n\u003Cp>ポートより先に名前を分ける。compose が自動で付ける名前なら、コンテナ名・ネットワーク・volume の頭にプロジェクト名が付くので、プロジェクト名が worktree ごとに違えばぶつからない。例外は自分で固定した名前で、\u003Ccode>container_name:\u003C\u002Fcode> や、ネットワーク・volume の \u003Ccode>name:\u003C\u002Fcode> はプロジェクト名を変えても同じになる。私の環境ではベクトル DB と DB の管理画面に固定のコンテナ名が残っていたので、移行のときに外した（共有する DB とプロキシは、逆に固定の名前をわざと付けている）。\u003C\u002Fp>\n\u003Cp>プロジェクト名は後でホスト名にも使うので、DNS のラベルとして使える形にする。gtr で作る worktree は、作成時のフックが \u003Ccode>&lt;リポジトリ名&gt;-&lt;worktree のディレクトリ名&gt;\u003C\u002Fcode> を小文字・ハイフン区切りにして \u003Ccode>.env\u003C\u002Fcode> に書く。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"bash\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">BASH\u003C\u002Fspan>\u003Cspan>5 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-bash\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># gtr の postCreate フック（抜粋）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">project_name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">=\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\"${repo_name}-${worktree_name}\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">project_name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">=\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\"$(\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\">printf\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> '%s' \"$project_name\" \\\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  |\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\"> tr\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> '[:upper:]' '[:lower:]' \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">|\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\"> tr\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -c\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> 'a-z0-9' '-' \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">|\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\"> tr\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -s\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> '-' \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">|\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\"> sed\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> 's\u002F^-\u002F\u002F; s\u002F-$\u002F\u002F')\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">printf\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> 'COMPOSE_PROJECT_NAME=%s\\n'\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> \"$project_name\"\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\"> >>\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> \"$WORKTREE_PATH\u002F.env\"\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>以前はアンダースコア区切りだったが、ホスト名にアンダースコアは使えないので変えた。gtr を通さずに作った worktree は、プロジェクト名を書かなければ compose の既定どおりディレクトリ名になる。\u003C\u002Fp>\n\u003Cp>\u003Ccode>.env\u003C\u002Fcode> は全 worktree で同じ中身にした。worktree に \u003Ccode>.env\u003C\u002Fcode> が無ければ、起動タスクがメインのチェックアウトの \u003Ccode>.env\u003C\u002Fcode> へシンボリックリンクを張る。このとき、共有元の \u003Ccode>.env\u003C\u002Fcode> に \u003Ccode>COMPOSE_PROJECT_NAME\u003C\u002Fcode> が書かれていると全 worktree が同じプロジェクトになってしまうので、その場合はリンクせずに止める。起動の直前には、同じプロジェクト名が別のディレクトリのコンテナに使われていないかも確かめている。\u003C\u002Fp>\n\u003Ch2 id=\"ポート:-ずらすのをやめ、入口を-1-つにしてホスト名で振り分ける\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">04\u003C\u002Fspan>ポート: ずらすのをやめ、入口を 1 つにしてホスト名で振り分ける\u003C\u002Fh2>\n\u003Cp>この章でやることを先に 1 枚で示す。左が以前の形、右が今の形だ。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fstatic.zenn.studio\u002Fuser-upload\u002F1d40fabd03e5-20261001.png\" alt=\"ポートずらしと入口 1 つの比較。Before では main・worktree A・worktree B がそれぞれ frontend を 3000・3001・3002、backend を 8080・8081・8082 で公開し、作成フックが 8 種類のポートを採番するため、フックを通らない worktree は衝突する。After ではリバースプロキシの 3000 番だけを開き、myapp.localhost・myapp-feat-a.localhost・myapp-feat-b.localhost のホスト名で各 worktree に振り分ける。各 worktree は ports を持たず、作り方を問わない\" loading=\"lazy\">\u003C\u002Fp>\n\u003Ch3 id=\"以前はポートをずらしていた\" tabindex=\"-1\">以前はポートをずらしていた\u003C\u002Fh3>\n\u003Cp>worktree を作るときのフックで、ほかの worktree の \u003Ccode>.env\u003C\u002Fcode> を読んで空いているオフセットを探し、8 種類のポートに同じ値を足して書き込んでいた。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"bash\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">BASH\u003C\u002Fspan>\u003Cspan>4 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-bash\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># 旧フック（抜粋）。8 種類のポートに同じオフセットを足して .env に書く\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">PORT_KEYS\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">=\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-foreground)\">(\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\">FRONTEND_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> STORYBOOK_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> BACKEND_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> AI_GATEWAY_PORT\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-string)\">           DB_GUI_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> FRONTEND_DEBUG_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> BACKEND_DEBUG_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> AI_GATEWAY_DEBUG_PORT\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-foreground)\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">PORT_BASES\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">=\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-foreground)\">(\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\">3000\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 6006\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 8080\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 8000\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 5050\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 9229\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 40000\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> 5678\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-foreground)\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>2 つ目の worktree なら frontend が 3001、backend が 8081、という具合だ。\u003Ca href=\"\u002Fblog\u002Fghostty-herdr-dev-env\">Ghostty + herdr の開発環境の記事\u003C\u002Fa>を書いた時点では、まだこの方式だった。動きはした。ただ、使い続けるうちに困ることが増えてきた。\u003C\u002Fp>\n\u003Col>\n\u003Cli>フックを通らない worktree は採番されない。\u003Ccode>git worktree add\u003C\u002Fcode> で作った worktree や、AI エージェント（私は TAKT を使っている）が作る独立したクローンは既定のポートのまま起動して衝突した。エージェントが作業している途中の画面を見られなかったのが一番困った\u003C\u002Fli>\n\u003Cli>ポートや URL を使う設定を足すたびにフックの修正が要り、追従漏れが出た。ベクトル DB のポート、メール内リンクの基点 URL、エディタのデバッグ設定が、それぞれ漏れていた\u003C\u002Fli>\n\u003Cli>URL がポート番号でしか区別できない。\u003Ccode>localhost:3002\u003C\u002Fcode> がどのブランチなのか覚えていられない\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 id=\"仕組み:-ホスト名で転送先を選ぶ\" tabindex=\"-1\">仕組み: ホスト名で転送先を選ぶ\u003C\u002Fh3>\n\u003Cp>ホストに開くポートをリバースプロキシの 1 つだけにする。ブラウザは \u003Ccode>http:\u002F\u002Fmyapp-feat-a.localhost:3000\u003C\u002Fcode> のようにホスト名を変えてアクセスし、プロキシはリクエストの Host ヘッダを見て、その worktree のコンテナへ転送する。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fstatic.zenn.studio\u002Fuser-upload\u002F529f65f2dc5b-20261001.png\" alt=\"ホスト名とパスで転送先を選ぶ図。myapp-feat-a.localhost:3000\u002F は worktree A の frontend へ、myapp-feat-a.localhost:3000\u002Fapi\u002Fv1\u002F 以下は \u002Fapi\u002Fv1 を外して worktree A の backend へ、myapp-feat-b.localhost:3000\u002F は worktree B の frontend へ転送される。リバースプロキシは Host ヘッダとパスを見て、ルールはコンテナのラベルから自動で作る。画面と API が同じホスト名なので同じオリジンになり、cookie と CSRF の設定が 1 つで済む\" loading=\"lazy\">\u003C\u002Fp>\n\u003Cp>\u003Ccode>*.localhost\u003C\u002Fcode> は Chrome と Firefox が 127.0.0.1 に解決するので、\u003Ccode>\u002Fetc\u002Fhosts\u003C\u002Fcode> も DNS の設定も要らない。\u003Ca href=\"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6761#section-6.3\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6761 §6.3\u003C\u002Fa> は localhost とそのサブドメインをループバックに解決するよう求めている。\u003C\u002Fp>\n\u003Cp>プロキシには Traefik を使った。Traefik は Docker のコンテナに付いたラベルを読んで、転送のルールを自動で作る。worktree のアプリが起動すればルールが増え、止めれば消える。コンテナのラベルから設定を拾えるプロキシなら、Traefik でなくても同じ形は作れる。\u003C\u002Fp>\n\u003Ch3 id=\"設定\" tabindex=\"-1\">設定\u003C\u002Fh3>\n\u003Cp>プロキシは、どの worktree にも属さない別のプロジェクトとして 1 つだけ起動する。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>24 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># compose\u002Fproxy.yaml（共有リバースプロキシ）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">services\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  proxy\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    image\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> traefik:v3.6\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # 実際はダイジェストで固定している\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    container_name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    restart\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> unless-stopped\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    command\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">--providers.docker=true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">      # traefik.enable=true のラベルを付けたコンテナだけを公開する\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">--providers.docker.exposedbydefault=false\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">--providers.docker.network=myapp_shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">--entrypoints.web.address=:80\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    ports\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\"127.0.0.1:3000:80\"\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # LAN には公開しない\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    volumes\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">      # コンテナのラベルを読むため\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock:ro\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  shared_proxy\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    external\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> true\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>起動するときは、ネットワークが無ければ作ってから、固定のプロジェクト名で \u003Ccode>up\u003C\u002Fcode> する。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"bash\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">BASH\u003C\u002Fspan>\u003Cspan>3 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-bash\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">docker\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> network\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> inspect\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> myapp_shared_proxy\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\"> >\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\">\u002Fdev\u002Fnull\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\"> 2>&#x26;1\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-foreground)\"> \\\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  ||\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\"> docker\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> network\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> create\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> myapp_shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">docker\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> compose\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> --project-name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> myapp_shared_proxy\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -f\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> compose\u002Fproxy.yaml\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -d\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> proxy\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>各 worktree の側は、アプリのコンテナにラベルを付けて共有ネットワークに参加させる。ホスト名にプロジェクト名を埋め込むので、同じ定義のまま worktree ごとに別のホスト名になる。\u003Ccode>COMPOSE_PROJECT_NAME\u003C\u002Fcode> を \u003Ccode>.env\u003C\u002Fcode> に書いていない worktree でも、compose がプロジェクト名を入れて展開した（Compose 5.1.2 で確認）。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>19 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># compose\u002Frouting.yaml（各 worktree の frontend をプロキシに登録する）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">services\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  frontend\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    environment\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">NEXT_PUBLIC_API_BASE_URL=\u002Fapi\u002Fv1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    labels\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.enable=true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.docker.network=myapp_shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\"traefik.http.routers.${COMPOSE_PROJECT_NAME}-frontend.rule=Host(`${COMPOSE_PROJECT_NAME}.localhost`)\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.http.routers.${COMPOSE_PROJECT_NAME}-frontend.entrypoints=web\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.http.services.${COMPOSE_PROJECT_NAME}-frontend.loadbalancer.server.port=3000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">default\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  shared_proxy\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    external\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> true\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>そして、アプリ側からは \u003Ccode>ports:\u003C\u002Fcode> を外す。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"diff\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">DIFF\u003C\u002Fspan>\u003Cspan>7 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-text\">\u003Cspan class=\"line\">\u003Cspan> services:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   frontend:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-    ports:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-      - \"${FRONTEND_PORT:-3000}:3000\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   backend:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-    ports:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-      - \"${BACKEND_PORT:-8080}:8080\"\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>これでホストに開く HTTP のポートは、プロキシの 3000 番だけになった。URL の \u003Ccode>:3000\u003C\u002Fcode> は全 worktree で同じだが、ホスト名が違うので行き先も違う。\u003C\u002Fp>\n\u003Ch3 id=\"フロントと-api-を同じオリジンにする\" tabindex=\"-1\">フロントと API を同じオリジンにする\u003C\u002Fh3>\n\u003Cp>ホスト名で振り分けるついでに、1 つのホスト名をパスで分けて、\u003Ccode>\u002Fapi\u002Fv1\u003C\u002Fcode> から始まるリクエストを backend に送るようにした。backend のルートは \u003Ccode>\u002F\u003C\u002Fcode> 直下にあるので、プロキシで \u003Ccode>\u002Fapi\u002Fv1\u003C\u002Fcode> を外してから転送する。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>20 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># compose\u002Frouting.yaml（続き）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">backend\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  environment\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">    # 許可元はこの worktree の URL 1 つだけ\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">AUTH_CSRF_TRUSTED_ORIGINS=http:\u002F\u002F${COMPOSE_PROJECT_NAME}.localhost:3000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">    # メール内リンクの基点もこの worktree の URL にそろえる\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">APP_BASE_URL=http:\u002F\u002F${COMPOSE_PROJECT_NAME}.localhost:3000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  labels\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.enable=true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.docker.network=myapp_shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">    # \u002Fapi\u002Fv10 などを拾わないよう、Path と PathPrefix を分けて書く\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\"traefik.http.routers.${COMPOSE_PROJECT_NAME}-api.rule=Host(`${COMPOSE_PROJECT_NAME}.localhost`) &#x26;&#x26; (Path(`\u002Fapi\u002Fv1`) || PathPrefix(`\u002Fapi\u002Fv1\u002F`))\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.http.routers.${COMPOSE_PROJECT_NAME}-api.entrypoints=web\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.http.routers.${COMPOSE_PROJECT_NAME}-api.middlewares=${COMPOSE_PROJECT_NAME}-api-strip\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.http.middlewares.${COMPOSE_PROJECT_NAME}-api-strip.stripprefix.prefixes=\u002Fapi\u002Fv1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">traefik.http.services.${COMPOSE_PROJECT_NAME}-api.loadbalancer.server.port=8080\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">default\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">shared_db\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # 共有 PostgreSQL 用（DB の章）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">shared_proxy\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>frontend は API の基点を相対パスの \u003Ccode>\u002Fapi\u002Fv1\u003C\u002Fcode> にしてあるので、どの worktree でも自分の backend を呼ぶ。変更前後の URL を並べるとこうなる。\u003C\u002Fp>\n\u003Cdiv class=\"tbl\" tabindex=\"0\" role=\"region\" aria-label=\"Table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>状態\u003C\u002Fth>\n\u003Cth>画面\u003C\u002Fth>\n\u003Cth>API\u003C\u002Fth>\n\u003Cth>オリジン\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>変更前（2 つ目の worktree）\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:3001\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:8081\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>別（ポートが違う）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>変更後\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Fmyapp-feat-a.localhost:3000\u002F\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Fmyapp-feat-a.localhost:3000\u002Fapi\u002Fv1\u002F\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>同じ\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>同じオリジンになって、cookie と CSRF の扱いが単純になった。backend は refresh cookie を Domain 属性なし（ホスト限定）・\u003Ccode>Path=\u002F\u003C\u002Fcode> で発行している。画面へのリクエストにもこの cookie が付くので、frontend の middleware がそのまま読める。ホスト名が worktree ごとに違うので、cookie も worktree ごとに分かれる。CSRF の許可元も、以前はフックが worktree ごとのポートから組み立てて \u003Ccode>.env\u003C\u002Fcode> に書いていたが、今は compose がプロジェクト名から組み立てる。\u003C\u002Fp>\n\u003Cp>Next.js の dev サーバーは \u003Ccode>localhost\u003C\u002Fcode> とそのサブドメインを既定で許可するので、\u003Ca href=\"https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fapp\u002Fapi-reference\u002Fconfig\u002Fnext-config-js\u002FallowedDevOrigins\" target=\"_blank\" rel=\"noopener noreferrer\">\u003Ccode>allowedDevOrigins\u003C\u002Fcode>\u003C\u002Fa> は設定していない。ホットリロード（HMR）の WebSocket もプロキシ越しにつながった。\u003C\u002Fp>\n\u003Ch3 id=\"動作確認\" tabindex=\"-1\">動作確認\u003C\u002Fh3>\n\u003Cfigure class=\"code\" data-lang=\"bash\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">BASH\u003C\u002Fspan>\u003Cspan>5 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-bash\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># この worktree のプロジェクト名（= ホスト名）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">docker\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> compose\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> config\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\"> |\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-function)\"> sed\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -n\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> '1s\u002F^name: \u002F\u002Fp'\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\">   # → myapp-feat-a\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">curl\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -s\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -o\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> \u002Fdev\u002Fnull\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -w\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> '%{http_code}\\n'\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> http:\u002F\u002Fmyapp-feat-a.localhost:3000\u002F\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\">          # frontend\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">curl\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -s\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -o\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> \u002Fdev\u002Fnull\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -w\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> '%{http_code}\\n'\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> http:\u002F\u002Fmyapp-feat-a.localhost:3000\u002Fapi\u002Fv1\u002F\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\">   # backend\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>どちらも 200 が返れば振り分けはできている。curl は \u003Ca href=\"https:\u002F\u002Fcurl.se\u002Fch\u002F7.85.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">7.85.0\u003C\u002Fa> から \u003Ccode>*.localhost\u003C\u002Fcode> を自分でループバックアドレス（127.0.0.1 と ::1）に解決する。\u003C\u002Fp>\n\u003Ch3 id=\"ほかの解き方と比べて\" tabindex=\"-1\">ほかの解き方と比べて\u003C\u002Fh3>\n\u003Cp>この形に落ち着くまでには回り道があった。きっかけは Vercel Labs の \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fvercel-labs\u002Fportless\" target=\"_blank\" rel=\"noopener noreferrer\">portless\u003C\u002Fa> だ。ホストで起動した dev サーバーに \u003Ccode>myapp.localhost\u003C\u002Fcode> のような名前付きの URL を付けるツールで、git worktree の中ではブランチ名をホスト名に入れてくれる。紹介記事を読んで、これを入れればポートずらしのフックを消せるのではないかと思い、調べ始めた。\u003C\u002Fp>\n\u003Cp>調べてみると、portless の自動の機能（\u003Ccode>PORT\u003C\u002Fcode> の割り当てや、ブランチ名からのホスト名）は、portless 経由で起動したプロセスにしか効かない。私の環境はアプリを全部コンテナで動かしているので、そのままでは効かない。\u003Ccode>portless alias &lt;名前&gt; &lt;ポート&gt;\u003C\u002Fcode> で、コンテナがホストに公開したポートに名前を付けることはできる。ただ、公開するポートを worktree ごとにずらす作業は残る。フックを消せないなら、入れる意味は薄い。\u003C\u002Fp>\n\u003Cp>次に見たのが、Docker の実行環境に使っている OrbStack の\u003Ca href=\"https:\u002F\u002Fdocs.orbstack.dev\u002Fdocker\u002Fdomains\" target=\"_blank\" rel=\"noopener noreferrer\">ドメイン機能\u003C\u002Fa>だ。Compose のサービスに \u003Ccode>service.project.orb.local\u003C\u002Fcode> の名前が自動で付き、設定はほとんど要らない。ただ OrbStack は macOS 向けのアプリで、商用利用には有料の\u003Ca href=\"https:\u002F\u002Fdocs.orbstack.dev\u002Flicensing\" target=\"_blank\" rel=\"noopener noreferrer\">ライセンス\u003C\u002Fa>が要る。Windows の人もいるチームで標準にするのは難しかった。\u003C\u002Fp>\n\u003Cp>そこで、全部 Compose のまま、プロキシも Compose で 1 つ立てる形にした。判断の基準はこう置いた。\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Mac と Windows で同じ手順で動く\u003C\u002Fli>\n\u003Cli>Docker で環境を再現できる状態を保つ\u003C\u002Fli>\n\u003Cli>worktree の作り方（gtr、\u003Ccode>git worktree add\u003C\u002Fcode>、エージェントのクローン）に依存しない\u003C\u002Fli>\n\u003Cli>CI の起動経路を壊さない\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cdiv class=\"tbl\" tabindex=\"0\" role=\"region\" aria-label=\"Table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>方式\u003C\u002Fth>\n\u003Cth>前提\u003C\u002Fth>\n\u003Cth>今回合わなかった点\u003C\u002Fth>\n\u003Cth>入れるときのコスト\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>ポートをずらす（従来）\u003C\u002Ftd>\n\u003Ctd>worktree 作成時にフックを通す\u003C\u002Ftd>\n\u003Ctd>フックを通らない worktree が漏れる。設定の追従漏れ\u003C\u002Ftd>\n\u003Ctd>フックの保守\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>portless（0.9.0 以降の既定）\u003C\u002Ftd>\n\u003Ctd>アプリを portless 経由でホストで起動\u003C\u002Ftd>\n\u003Ctd>自動の機能がコンテナに効かず、ポートの採番が残る\u003C\u002Ftd>\n\u003Ctd>443 番に sudo で常駐する。ローカル CA をトラストストアに入れる\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>OrbStack のドメイン\u003C\u002Ftd>\n\u003Ctd>OrbStack を Docker の実行環境に使う\u003C\u002Ftd>\n\u003Ctd>macOS 向けで、商用利用は有料\u003C\u002Ftd>\n\u003Ctd>ほぼ設定不要\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>devcontainer\u003C\u002Ftd>\n\u003Ctd>エディタからコンテナに入って開発する\u003C\u002Ftd>\n\u003Ctd>ツールの再現性はそろうが、ポートの問題は解かない\u003C\u002Ftd>\n\u003Ctd>設定ファイル一式\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>共有リバースプロキシ（採用）\u003C\u002Ftd>\n\u003Ctd>Docker（実行環境は問わない）\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>3000 番を 1 つ占有する。HTTP 以外は対象外\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>アプリをホストで直接起動している構成なら、portless はかなり手軽な選択肢だと思う。私の場合に合わなかったのは、全部を Compose で動かしていたからだ。\u003C\u002Fp>\n\u003Ch3 id=\"ポートずらしの頃と比べて変わったこと\" tabindex=\"-1\">ポートずらしの頃と比べて変わったこと\u003C\u002Fh3>\n\u003Cdiv class=\"tbl\" tabindex=\"0\" role=\"region\" aria-label=\"Table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>項目\u003C\u002Fth>\n\u003Cth>ポートずらし\u003C\u002Fth>\n\u003Cth>共有リバースプロキシ\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>URL\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:3001\u003C\u002Fcode>（番号で区別）\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Fmyapp-feat-a.localhost:3000\u003C\u002Fcode>（名前で区別）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>ホストに開く HTTP のポート\u003C\u002Ftd>\n\u003Ctd>worktree ごとに最大 6 つ（ツール用を含む）\u003C\u002Ftd>\n\u003Ctd>全体でプロキシの 1 つ\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>衝突しない worktree\u003C\u002Ftd>\n\u003Ctd>gtr で作ったものだけ\u003C\u002Ftd>\n\u003Ctd>作り方を問わない\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>worktree 作成フック\u003C\u002Ftd>\n\u003Ctd>169 行（大半がポートの採番）\u003C\u002Ftd>\n\u003Ctd>69 行（名前の設定と依存の install）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>CSRF の許可元\u003C\u002Ftd>\n\u003Ctd>フックがポートから生成して \u003Ccode>.env\u003C\u002Fcode> に書く\u003C\u002Ftd>\n\u003Ctd>compose がホスト名から組み立てる\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>LAN への公開\u003C\u002Ftd>\n\u003Ctd>frontend などを全インターフェースに公開\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>127.0.0.1\u003C\u002Fcode> だけ\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch2 id=\"db:-1-つだけ立てて、全-worktree-から同じ名前で参照する\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">05\u003C\u002Fspan>DB: 1 つだけ立てて、全 worktree から同じ名前で参照する\u003C\u002Fh2>\n\u003Cp>PostgreSQL は全 worktree で 1 つを共有している。これはプロキシより前、2026 年 7 月から続けている構成だ。\u003C\u002Fp>\n\u003Ch3 id=\"仕組み\" tabindex=\"-1\">仕組み\u003C\u002Fh3>\n\u003Cp>共有の DB も、プロキシと同じく別のプロジェクトとして 1 つだけ起動し、専用のネットワーク \u003Ccode>myapp_shared_db\u003C\u002Fcode> に \u003Ccode>db\u003C\u002Fcode> という別名で参加させる。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>23 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># compose\u002Fdb.yaml（共有 PostgreSQL）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">services\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  db\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    image\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> postgres:16-alpine\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    container_name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_postgres\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    restart\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> always\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    ports\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">\"127.0.0.1:5432:5432\"\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # ホストの psql などから使う。LAN には公開しない\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    volumes\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">postgres_data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">      shared_db\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">        aliases\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">          - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">db\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">volumes\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  postgres_data\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_postgres_data\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # プロジェクト名に左右されない固定の名前\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  shared_db\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_shared_db\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    external\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> true\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>各 worktree の backend は、自分のプロジェクトの \u003Ccode>default\u003C\u002Fcode> ネットワークと、この共有ネットワークの両方に入る。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>11 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># compose\u002Fbase.yaml（各 worktree）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">services\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  backend\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">default\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # 自分の worktree の frontend やベクトル DB と話す\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">shared_db\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # 共有 PostgreSQL と話す\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">networks\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  shared_db\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    name\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> myapp_shared_db\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    external\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-constant)\"> true\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>\u003Cimg src=\"..\u002Fassets\u002F2026-10-01_worktree-shared-reverse-proxy\u002Ffig4-shared-db-network.png\" alt=\"共有 DB のネットワーク図。worktree A と worktree B のネットワークにはそれぞれ frontend・ベクトル DB・backend がある。backend だけが自分の worktree のネットワークと共有ネットワーク（myapp_shared_db）の両方に入り、共有ネットワーク上の PostgreSQL（別名 db）に db:5432 で接続する。接続先はどの worktree でも db:5432 で、frontend とベクトル DB は DB に触れない\" loading=\"lazy\">\u003C\u002Fp>\n\u003Cp>Docker のネットワークの中では、コンテナをサービス名や別名で引ける。backend から見ると、共有ネットワークの上に \u003Ccode>db\u003C\u002Fcode> という名前のホストが 1 つあるだけだ。接続文字列は \u003Ccode>postgres:\u002F\u002F...@db:5432\u002F...\u003C\u002Fcode> のまま、どの worktree でも変えずに済む。frontend とベクトル DB は共有ネットワークに入らないので、DB には直接触れない。\u003C\u002Fp>\n\u003Cp>以前は DB を各 worktree の compose に入れていた。volume はプロジェクトごとに分かれるので、同じ DB を使うつもりでもデータは別々になり、2 つ目の worktree を起動すると固定のコンテナ名と 5432 番で衝突した。DB を別のプロジェクトに切り出し、volume に固定の名前を付けたのはそのためだ。\u003C\u002Fp>\n\u003Ch3 id=\"なぜネットワークを共有するのか\" tabindex=\"-1\">なぜネットワークを共有するのか\u003C\u002Fh3>\n\u003Cp>DB の持ち方は、ほかにも考えられる。並べるとこうなる。\u003C\u002Fp>\n\u003Cdiv class=\"tbl\" tabindex=\"0\" role=\"region\" aria-label=\"Table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>方式\u003C\u002Fth>\n\u003Cth>良いところ\u003C\u002Fth>\n\u003Cth>困るところ\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>worktree ごとに DB を立てる\u003C\u002Ftd>\n\u003Ctd>スキーマとデータがブランチ間で干渉しない\u003C\u002Ftd>\n\u003Ctd>DB の数だけメモリとディスクを使う。データを使い回せない\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>ホスト経由でつなぐ（\u003Ccode>host.docker.internal\u003C\u002Fcode>）\u003C\u002Ftd>\n\u003Ctd>ネットワークを共有しなくてよい\u003C\u002Ftd>\n\u003Ctd>接続先の書き方が OS や Docker の実行環境に依存する\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>共有ネットワークに置く（採用）\u003C\u002Ftd>\n\u003Ctd>接続先 \u003Ccode>db:5432\u003C\u002Fcode> を変えずに 1 つを使い回せる\u003C\u002Ftd>\n\u003Ctd>migration やデータの変更が全 worktree に効く\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>共有にしておくと、worktree を作るたびにテスト用のユーザーやデータを作り直さずに済む。ホストから見たいときは \u003Ccode>127.0.0.1:5432\u003C\u002Fcode> で psql などをつなぐ。\u003C\u002Fp>\n\u003Ch3 id=\"共有して困るのは、スキーマが分かれるとき\" tabindex=\"-1\">共有して困るのは、スキーマが分かれるとき\u003C\u002Fh3>\n\u003Cp>共有 DB には向かない使い方がある。互換性のない migration を、ブランチごとに並行して試す使い方だ。\u003C\u002Fp>\n\u003Cul>\n\u003Cli>migration ツールが記録する版が、先に適用したブランチの版まで進む。その migration ファイルを持たない別のブランチでは、ツールが版を解決できずに止まることがある\u003C\u002Fli>\n\u003Cli>あるブランチで migration を戻す（down）と、そのテーブルを使っている別のブランチが壊れる\u003C\u002Fli>\n\u003Cli>DB をリセットすると、全 worktree のデータが消える\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>今のところ、これは README で「migration、DB のリセット、手動のデータ変更は、起動中の全 worktree に影響する」と注意しているだけで、仕組みでは防いでいない。\u003C\u002Fp>\n\u003Cp>中間の案もある。PostgreSQL のコンテナは 1 つのまま、worktree ごとに database（または schema）を分ける方法だ。\u003Ccode>CREATE DATABASE myapp_feat_a\u003C\u002Fcode> のように worktree ごとの DB を作り、接続文字列の DB 名だけを変える。データは使い回せなくなるが、スキーマは干渉しない。この案は当時の比較に入れていなかった。スキーマを変える作業を並行することが増えたら、次の候補になる。\u003C\u002Fp>\n\u003Ch3 id=\"ベクトル-db-は-worktree-ごとに分けた\" tabindex=\"-1\">ベクトル DB は worktree ごとに分けた\u003C\u002Fh3>\n\u003Cp>一方、ベクトル DB（Qdrant）は worktree ごとに持たせた。共有すると困ることがあるからだ。\u003C\u002Fp>\n\u003Cul>\n\u003Cli>E2E テストでは LLM を偽物に差し替えていて、意味のないベクトルが作られる。共有すると、それが実データに混ざる\u003C\u002Fli>\n\u003Cli>ブランチで埋め込みの方式やコレクションの形式を変えると、その変更が全 worktree に効いてしまう\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>この組み合わせには副作用がある。worktree A で登録したデータは PostgreSQL に入るので、worktree B の画面にも出る。ところがベクトルは A のベクトル DB にしか無いので、B の検索では使えない。B で使いたければ、B で索引を作り直す。\u003C\u002Fp>\n\u003Ch3 id=\"共有するか分けるかの目安\" tabindex=\"-1\">共有するか分けるかの目安\u003C\u002Fh3>\n\u003Cp>DB に限らず、何を共有するかは次の観点で決めた。\u003C\u002Fp>\n\u003Cul>\n\u003Cli>共有する: ブランチをまたいでデータを使い回したい。スキーマの変更がまれ。起動の手間とリソースを減らしたい\u003C\u002Fli>\n\u003Cli>分ける: ブランチで形式やスキーマが変わる。テスト用のデータが混ざると困る。作り直しが安い\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>PostgreSQL は前者、ベクトル DB と node_modules は後者だった。\u003C\u002Fp>\n\u003Ch2 id=\"依存と起動:-1-コマンドで、worktree-の作り方に依存しない\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">06\u003C\u002Fspan>依存と起動: 1 コマンドで、worktree の作り方に依存しない\u003C\u002Fh2>\n\u003Ch3 id=\"node_modules-は-worktree-ごとの-volume-に置く\" tabindex=\"-1\">node_modules は worktree ごとの volume に置く\u003C\u002Fh3>\n\u003Cp>イメージ名は全 worktree で共有している。worktree ごとに名前を分けると、消した worktree の分だけイメージが残るからだ。その代わり、起動のたびにその worktree のソースからビルドし直す（差分が無ければキャッシュですぐ終わる）。node_modules はイメージの中のものを使わず、worktree ごとの名前付き volume に置いて、起動時にその worktree の lockfile で入れ直す。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>12 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># compose\u002Foverride.yaml（ローカル開発用）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">services\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  frontend\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">    # lockfile が変わっていなければ数秒で終わる\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    command\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> sh -c \"pnpm install --frozen-lockfile &#x26;&#x26; exec pnpm run dev\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">    volumes\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">.\u002Ffrontend:\u002Fapp\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\">      # volume 名にプロジェクト名が付くので、worktree ごとに分かれる\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">      - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">frontend_node_modules:\u002Fapp\u002Fnode_modules\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">volumes\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  frontend_node_modules\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>pnpm の store もこの volume の中に置いた（Dockerfile で \u003Ccode>npm_config_store_dir=\u002Fapp\u002Fnode_modules\u002F.pnpm-store\u003C\u002Fcode>）。ハードリンクが効き、worktree を消せば volume と一緒に消える。2 回目以降の install は 1〜4 秒ほどで、volume が空の初回は十数秒かかる。\u003C\u002Fp>\n\u003Ch3 id=\"起動の流れ\" tabindex=\"-1\">起動の流れ\u003C\u002Fh3>\n\u003Cp>起動はタスクランナー（Task）の \u003Ccode>task compose:up\u003C\u002Fcode> 1 つにまとめた。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"yaml\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">YAML\u003C\u002Fspan>\u003Cspan>8 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-yaml\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-comment)\"># taskfiles\u002FTaskfile.compose.yml（抜粋）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-keyword)\">  cmds\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">task\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> env:link\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # .env が無ければメインの .env にリンク\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">task\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> project:check\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # プロジェクト名が別のディレクトリで使われていないか\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">task\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> db:up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # 共有 PostgreSQL（無ければ作り、あれば使う）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">task\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-keyword)\">:\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\"> proxy:up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # 共有プロキシ（同上）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-foreground)\">    - \u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string-expression)\">docker compose up -d --build\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\"> # この worktree のアプリ\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>Task を使わない場合は、次の 3 つを順に実行すれば同じことになる。\u003C\u002Fp>\n\u003Cfigure class=\"code\" data-lang=\"bash\">\u003Cdiv class=\"code-bar\">\u003Cspan class=\"lang\">BASH\u003C\u002Fspan>\u003Cspan>3 LINES\u003C\u002Fspan>\u003Cbutton class=\"copy\" type=\"button\" data-cursor=\"COPY\">COPY\u003C\u002Fbutton>\u003C\u002Fdiv>\u003Cpre class=\"shiki site-code\" style=\"background-color:var(--shiki-background);color:var(--shiki-foreground)\" tabindex=\"0\">\u003Ccode class=\"language-bash\">\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">.\u002Fscripts\u002Fshared-db.sh\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\">      # 共有ネットワーク + PostgreSQL（別名 db）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">.\u002Fscripts\u002Fshared-proxy.sh\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\">   # 共有ネットワーク + Traefik\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-token-function)\">docker\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> compose\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> up\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> -d\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-string)\"> --build\u003C\u002Fspan>\u003Cspan style=\"color:var(--shiki-token-comment)\">   # この worktree のアプリ\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003C\u002Ffigure>\n\u003Cp>共有の DB とプロキシは「無ければ作り、あれば使う」ので、最初に起動した worktree が作り、2 つ目以降はそれを使う。gtr で作った worktree でも、\u003Ccode>git worktree add\u003C\u002Fcode> で作ったものでも、AI エージェントのクローンでも、同じ \u003Ccode>task compose:up\u003C\u002Fcode> で起動できる。前提は、プロジェクト名が worktree ごとに違うこと。ディレクトリ名が同じ別のクローンなどで名前が重なったら、起動前のチェックが止める。名前を自動で付け直してくれるわけではない。エージェントが作業している途中の画面も、プロジェクト名の URL で開ける。\u003C\u002Fp>\n\u003Ch3 id=\"片付け\" tabindex=\"-1\">片付け\u003C\u002Fh3>\n\u003Cp>gtr で worktree を消すときは、削除前のフックで \u003Ccode>docker compose down --volumes\u003C\u002Fcode> を流す。gtr を通さずに消した worktree の残骸（コンテナ・ネットワーク・volume）は、\u003Ccode>task compose:prune\u003C\u002Fcode> で一覧にしてから消す。作業ディレクトリが存在しないプロジェクトを探し、共有の DB とプロキシは名前で除外する。不要になったイメージも、自分のプロジェクトのラベルが付いたものだけを消す（\u003Ccode>docker image prune\u003C\u002Fcode> はホスト全体が対象になるので使わない）。\u003C\u002Fp>\n\u003Ch3 id=\"ci-の-e2e-も同じ経路にした\" tabindex=\"-1\">CI の E2E も同じ経路にした\u003C\u002Fh3>\n\u003Cp>最初は CI だけ、frontend と backend をポートで公開した別オリジンのまま E2E を動かしていた。これだとローカルと CI で cookie と CSRF の条件がそろわない。そこで CI でもプロキシを起動し、\u003Ccode>http:\u002F\u002Fmyapp-ci.localhost:3000\u003C\u002Fcode> で E2E を流すようにした。GitHub Actions の runner では、Playwright の API リクエストなど OS の名前解決を使うクライアントのために、\u003Ccode>\u002Fetc\u002Fhosts\u003C\u002Fcode> に \u003Ccode>myapp-ci.localhost\u003C\u002Fcode> を登録している。\u003C\u002Fp>\n\u003Ch2 id=\"まとめ:-自分の環境に持ち込むチェックリスト\" tabindex=\"-1\">\u003Cspan class=\"h-no\" aria-hidden=\"true\">07\u003C\u002Fspan>まとめ: 自分の環境に持ち込むチェックリスト\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Ccode>docker compose config\u003C\u002Fcode> の出力から \u003Ccode>ports:\u003C\u002Fcode> と \u003Ccode>container_name:\u003C\u002Fcode> を洗い出す。固定の \u003Ccode>container_name:\u003C\u002Fcode> は外す\u003C\u002Fli>\n\u003Cli>プロジェクト名の付け方を決める（小文字・ハイフン区切りで、ホスト名に使える形）。\u003Ccode>.env\u003C\u002Fcode> を共有するなら、そこに \u003Ccode>COMPOSE_PROJECT_NAME\u003C\u002Fcode> を書かない\u003C\u002Fli>\n\u003Cli>共有するもの（DB など）を別のプロジェクトに切り出し、\u003Ccode>docker network create\u003C\u002Fcode> で作ったネットワークに別名付きで参加させる。volume には \u003Ccode>name:\u003C\u002Fcode> で固定の名前を付ける\u003C\u002Fli>\n\u003Cli>リバースプロキシを別のプロジェクトで 1 つ立て、アプリにラベルを付けて \u003Ccode>ports:\u003C\u002Fcode> を外す。ホストへの公開は \u003Ccode>127.0.0.1\u003C\u002Fcode> だけにする\u003C\u002Fli>\n\u003Cli>frontend の API の基点を相対パス（\u003Ccode>\u002Fapi\u002Fv1\u003C\u002Fcode>）にし、CSRF などの許可元を \u003Ccode>&lt;プロジェクト名&gt;.localhost\u003C\u002Fcode> から組み立てる\u003C\u002Fli>\n\u003Cli>「共有 DB → プロキシ → \u003Ccode>docker compose up -d --build\u003C\u002Fcode>」を 1 つのコマンドにまとめる\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>アプリをホストで直接起動しているなら portless、Mac で個人開発しているなら OrbStack のドメイン機能のほうが手軽だと思う。全部を Compose で動かしていて、作り方の違う worktree が混ざるなら、入口を 1 つにする形が合う。\u003C\u002Fp>\n\u003Cp>フックが 100 行減ったのはおまけだった。変わったのは、エージェントが作業中の worktree を、ポート番号を調べずにプロジェクト名の URL で開けるようになったことだ。\u003C\u002Fp>\n",[461,464,467,470,473,475,478,480,483,485,487,489,492,494,496,498,501,503,506,509,511,513,516],{"level":426,"id":462,"text":463},"やりたいこと:-各-worktree-のアプリを同時に起動して確かめたい","やりたいこと: 各 worktree のアプリを同時に起動して確かめたい",{"level":426,"id":465,"text":466},"結論:-入口と-db-は共有し、アプリは-worktree-ごとに分ける","結論: 入口と DB は共有し、アプリは worktree ごとに分ける",{"level":426,"id":468,"text":469},"まず-worktree-ごとに名前を分ける（compose-のプロジェクト名）","まず worktree ごとに名前を分ける（compose のプロジェクト名）",{"level":426,"id":471,"text":472},"ポート:-ずらすのをやめ、入口を-1-つにしてホスト名で振り分ける","ポート: ずらすのをやめ、入口を 1 つにしてホスト名で振り分ける",{"level":419,"id":474,"text":474},"以前はポートをずらしていた",{"level":419,"id":476,"text":477},"仕組み:-ホスト名で転送先を選ぶ","仕組み: ホスト名で転送先を選ぶ",{"level":419,"id":479,"text":479},"設定",{"level":419,"id":481,"text":482},"フロントと-api-を同じオリジンにする","フロントと API を同じオリジンにする",{"level":419,"id":484,"text":484},"動作確認",{"level":419,"id":486,"text":486},"ほかの解き方と比べて",{"level":419,"id":488,"text":488},"ポートずらしの頃と比べて変わったこと",{"level":426,"id":490,"text":491},"db:-1-つだけ立てて、全-worktree-から同じ名前で参照する","DB: 1 つだけ立てて、全 worktree から同じ名前で参照する",{"level":419,"id":493,"text":493},"仕組み",{"level":419,"id":495,"text":495},"なぜネットワークを共有するのか",{"level":419,"id":497,"text":497},"共有して困るのは、スキーマが分かれるとき",{"level":419,"id":499,"text":500},"ベクトル-db-は-worktree-ごとに分けた","ベクトル DB は worktree ごとに分けた",{"level":419,"id":502,"text":502},"共有するか分けるかの目安",{"level":426,"id":504,"text":505},"依存と起動:-1-コマンドで、worktree-の作り方に依存しない","依存と起動: 1 コマンドで、worktree の作り方に依存しない",{"level":419,"id":507,"text":508},"node_modules-は-worktree-ごとの-volume-に置く","node_modules は worktree ごとの volume に置く",{"level":419,"id":510,"text":510},"起動の流れ",{"level":419,"id":512,"text":512},"片付け",{"level":419,"id":514,"text":515},"ci-の-e2e-も同じ経路にした","CI の E2E も同じ経路にした",{"level":426,"id":517,"text":518},"まとめ:-自分の環境に持ち込むチェックリスト","まとめ: 自分の環境に持ち込むチェックリスト",27,{"slug":61,"title":62,"description":63,"publishedAt":64,"updatedAt":64,"category":521,"tags":522,"thumbnail":16},{"name":49,"slug":50},[523,524,525,526],{"name":58,"slug":59},{"name":69,"slug":69},{"name":71,"slug":71},{"name":73,"slug":73},{"slug":36,"title":37,"description":38,"publishedAt":39,"updatedAt":39,"category":528,"tags":529,"thumbnail":16},{"name":11,"slug":12},[530],{"name":25,"slug":26},[532,539,545],{"slug":61,"title":62,"description":63,"publishedAt":64,"updatedAt":64,"category":533,"tags":534,"thumbnail":16},{"name":49,"slug":50},[535,536,537,538],{"name":58,"slug":59},{"name":69,"slug":69},{"name":71,"slug":71},{"name":73,"slug":73},{"slug":75,"title":76,"description":77,"publishedAt":78,"updatedAt":78,"category":540,"tags":541,"thumbnail":16},{"name":49,"slug":50},[542,543,544],{"name":58,"slug":59},{"name":83,"slug":84},{"name":86,"slug":86},{"slug":168,"title":169,"description":170,"publishedAt":171,"updatedAt":172,"category":546,"tags":547,"thumbnail":550},{"name":49,"slug":50},[548,549],{"name":25,"slug":26},{"name":58,"slug":59},{"url":178,"description":104},1793725976654]