プログラマのための IT 教科書

開発環境を作る

この部の 8 / 8 章 ・ 全体で 12 / 76 章 ・ 読了目安 45 分

この章を読むとできるようになること
  • 手元にサービスと依存を一式立ち上げられる
  • 「自分の環境だけ動かない」の原因を切り分けられる
  • デバッガを使って実行を止めて中を見られる

配属初日の午前中、あなたが最初にやるのは機能開発ではありません。環境構築です。 そしてここは、教科書がいちばん手薄で、いちばん時間が溶ける場所でもあります。

この章では、リポジトリを clone してから「手元でアプリが動いてブレークポイントが止まる」までを扱います。 コマンドと設定ファイルをそのまま載せるので、読みながら手を動かしてください。

言語のバージョンを管理する

なぜシステムの Go / Node を直接使わないのか

brew install node で入れた Node は、そのマシンに1つだけ存在します。 一方、現場のリポジトリはこうなっています。

リポジトリNodeGo
api-gateway22.x1.24
admin-web20.x(まだ上げていない)—
batch-worker—1.22(依存ライブラリの都合)

3つを行き来する日常で、Node が1つしかないとどうなるでしょうか。 admin-web を動かすために 20 に下げると、api-gateway のビルドが壊れます。 戻すと逆が壊れます。1日に何度も入れ替える作業が発生します。

さらに厄介なのは、バージョンがずれてもすぐには落ちないことです。 ビルドは通るのに実行時だけ挙動が違う、CI では落ちるのにローカルでは通る、といった形で出てきます。

そしてチーム全員に「Node は 22.14.0 を入れてください」と口頭で伝える運用は必ず失敗します。 新しく入った人に伝わりませんし、バージョンを上げた時にも伝わりません。 必要なバージョンはリポジトリの中に書きます。 人ではなくツールが読む形にするのが要点です。

バージョンマネージャを使う

言語ごとにバージョンを切り替えるツールをバージョンマネージャと呼びます。 Node なら nvm、Go なら goenv、Python なら pyenv……と言語ごとに別のツールがありますが、 全部まとめて1つで面倒を見るタイプのものを使うのが今の主流です。

  • mise(旧 rtx)— 速い。設定が1ファイル。最近はこれが第一候補
  • asdf — 歴史が長く、プラグインが豊富。現場に既にあるならこのまま使う

どちらも「リポジトリのディレクトリに入った瞬間、そのプロジェクト用のバージョンに切り替わる」という点は同じです。

brew install mise
 
# ~/.zshrc に1行足す(これがないと切り替えが効かない)
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc
source ~/.zshrc

プロジェクトで使うバージョンを指定します。

cd ~/work/api-gateway
 
mise use node@22.14.0     # .tool-versions に書き込まれる
mise use go@1.24.2
 
mise install              # 書かれているものを全部入れる
mise current              # 今このディレクトリで有効なバージョン

mise current の出力と node --version が一致していれば成功です。

.tool-versions をコミットする

mise use が作るのがこのファイルです。

node 22.14.0
go 1.24.2

たった2行ですが、これをリポジトリにコミットすることには大きな意味があります。

  1. clone した人が mise install だけで環境を揃えられる(口頭伝達が不要になる)
  2. バージョンを上げる時に PR のレビュー対象になる(誰かが勝手に上げて壊すことがなくなる)
  3. いつ誰が上げたかが git log に残る(git log -p .tool-versions で追える)

これは「設定をコードとして扱う」という考え方の、いちばん小さくていちばん効く実践例です。

mise を入れたのにバージョンが切り替わらない

マシンとシェルの PATH の話が、そのまま効いてきます。

which node
# /opt/homebrew/bin/node        ← Homebrew の Node が使われている(切り替わっていない)
# ~/.local/share/mise/installs/node/22.14.0/bin/node   ← 正しい状態

原因はほぼこの2つです。

  1. mise activate を .zshrc に書いていない(書いたが source していない)
  2. Homebrew の Node が PATH の左側にいて、先に見つかっている

which で現実を確認してから直してください。再インストールしても直りません。

依存サービスをローカルで立てる

アプリは単体では動きません。DB があり、キャッシュがあり、場合によってはメッセージキューがあります。 これを手元にどう用意するかが次の問題です。

昔は各自のマシンに Postgres を直接インストールしていましたが、今はやりません。理由は明快です。

直接インストールDocker で立てる
バージョンはマシンに1つプロジェクトごとに別バージョンを同時起動できる
消すのが面倒(設定が残る)docker compose down -v で完全に消える
手順が OS ごとに違うどの OS でも同じコマンド
壊れたら復旧が大変壊れたら作り直せばよい

Docker Compose の最小構成

compose.yaml をリポジトリのルートに置きます(古い名前の docker-compose.yml でも動きます)。

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_development
    ports:
      - "5432:5432"
    volumes:
      - db-data:/var/lib/postgresql/data
      # 初回起動時だけ実行される初期化 SQL
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app_development"]
      interval: 5s
      timeout: 3s
      retries: 10
 
  api:
    build: .
    environment:
      # ホスト名は localhost ではなくサービス名 db
      DATABASE_URL: postgres://app:password@db:5432/app_development
      PORT: "8080"
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - .:/app
      - /app/node_modules
 
volumes:
  db-data:

読むべきポイントは4つです。

  • ports — "ホスト側:コンテナ側" の順です。順序を逆にすると繋がりません
  • volumes — db-data は名前付きボリューム。コンテナを消してもデータが残ります
  • depends_on + condition: service_healthy — DB が受け付けられる状態になるまで待ちます
  • DATABASE_URL のホスト名が db — ここが最大の罠です。次節で扱います

3つ目について補足します。depends_on: [db] とだけ書いた場合、Compose が待つのは 「コンテナが起動したこと」であって「DB が接続を受け付けられること」ではありません。 Postgres はプロセスが立ってから初期化を終えるまでに数秒かかるので、 その間にアプリが接続しにいって connection refused で落ちる、という事故が起きます。 healthcheck と condition: service_healthy を書くか、アプリ側で接続をリトライする実装にしてください。 本番でも DB が一瞬落ちることはあるので、リトライは結局必要になります。

起動・確認・後始末

docker compose up -d          # バックグラウンドで起動
docker compose ps             # 何が動いているか、状態は healthy か
docker compose logs -f api    # api のログを流し続ける(Ctrl+C で抜ける)
docker compose exec db psql -U app app_development   # コンテナの中でコマンドを打つ
docker compose restart api    # 1つだけ再起動
docker compose down           # 停止して削除。ボリュームは残る
docker compose down -v        # ボリュームごと削除。DB のデータは消える

この使い分けは覚えてください。

コマンドいつ使うか
up -d朝いちばん。作業開始時
logs -f <service>動かない時。まず最初にこれ
restart <service>設定を変えた時。起動は速い
up -d --buildDockerfile や依存を変えた時。イメージを作り直す
down作業終了時。データは残る
down -vDB を初期状態に戻したい時だけ
down -v は取り返しがつきません

-v はボリュームを削除します。つまりローカル DB のデータが全部消えます。

「動かないからとりあえず down -v」を癖にすると、 半日かけて投入したテストデータを吹き飛ばします。

  • まず logs -f で原因を見る
  • スキーマがおかしい、マイグレーションが中途半端、という時に初めて -v を使う

なお -v が消すのはこのプロジェクトのボリュームだけです。他のプロジェクトには影響しません。

コンテナの中と外

ここが新人が最も踏む場所です。コンテナの中は、外とは別のネットワークです。

localhost は「自分自身」を指す

localhost は「今このプログラムが動いているマシン」を意味します。 コンテナの中で localhost と書くと、それはそのコンテナ自身を指します。ホストマシンではありません。

あなたの Mac(ホスト)
├── ターミナル       →  localhost:5432 は Mac 自身 → ポート公開されていれば DB に届く
│
└── Docker
    ├── api コンテナ  →  localhost:5432 は api コンテナ自身 → DB はいない → 接続失敗
    └── db コンテナ   →  他のコンテナからは "db" という名前で見える

つまり、同じ DATABASE_URL でも書く場所によって正解が変わります。

どこから接続するか正しいホスト名
Mac のターミナルや GUI クライアントからlocalhost:5432
api コンテナから db コンテナへdb:5432(Compose のサービス名)
コンテナから Mac 上で直接動かしているサーバーへhost.docker.internal
connection refused の9割はこれ

.env に DATABASE_URL=postgres://app:password@localhost:5432/app_development と書いて、 アプリをコンテナで動かすと必ず失敗します。

dial tcp 127.0.0.1:5432: connect: connection refused

エラーメッセージに 127.0.0.1 が出ていたら、それが答えです。 コンテナが自分自身に繋ぎにいって、誰もいなかった、という意味です。

アプリをコンテナで動かすなら db、ホストで動かすなら localhost。 チームによっては「アプリだけホストで動かし、DB はコンテナ」という構成もあるので、 自分がどちらで動かしているかを先に確認してください。

ポートフォワード

ports: - "8080:8080" は、ホストの 8080 をコンテナの 8080 に繋ぐという宣言です。 これを書かないと、コンテナの中でサーバーが元気に動いていても、ブラウザからは一切見えません。

ports:
  - "5433:5432"   # ホストの 5433 → コンテナの 5432

左をずらすのは、すでにホストで別の Postgres が 5432 を使っている時の定番の逃げ方です。

lsof -i :5432                 # 誰がそのポートを使っているか
docker compose port api 8080  # api の 8080 がホストのどこに出ているか

もう1つ、アプリが 127.0.0.1 にだけバインドしているとポートを公開しても届きません。 コンテナの中では 0.0.0.0 で待ち受ける必要があります。

# コンテナ内では 0.0.0.0 で listen する
next dev --hostname 0.0.0.0

ボリュームの2つの使い道

volumes:
  - db-data:/var/lib/postgresql/data   # 名前付きボリューム: データを永続化する
  - .:/app                             # バインドマウント: ホストのコードを中に見せる
  - /app/node_modules                  # 匿名ボリューム: 上の穴埋め

3行目が分かりにくいので説明します。 .:/app でホストのディレクトリを丸ごと被せると、イメージのビルド時にコンテナ内へ入れた node_modules が隠されます。 ホスト側の node_modules(無い、あるいは macOS 用にビルドされた別物)が見えてしまい、 Cannot find module や「ネイティブモジュールが実行できない」という形で失敗します。

/app/node_modules と書いておくと、そこだけホスト側で上書きされなくなります。

中で何が起きているかは exec で見る

推測せず、コンテナの中に入って確認するのが速いです。

docker compose exec api sh          # シェルに入る
docker compose exec api env         # 環境変数が届いているか
docker compose exec api ls /app     # ファイルがどう見えているか
docker compose exec api sh -c 'nc -zv db 5432'   # db に到達できるか

「設定したはず」を「設定されている」に変えるだけで、調査時間が桁で変わります。

環境変数と設定

.env はコミットしない、.env.example はコミットする

アプリの設定値(接続先、APIキー、フラグ)は環境変数で渡すのが基本です。 ローカルではそれを .env というファイルに書きます。

# .env  ← これは絶対にコミットしない
DATABASE_URL=postgres://app:password@db:5432/app_development
STRIPE_SECRET_KEY=sk_test_51Hxxxxxxxxxxxxxxxx
LOG_LEVEL=debug
# .env.example  ← これはコミットする
DATABASE_URL=postgres://app:password@db:5432/app_development
STRIPE_SECRET_KEY=      # 開発用のキーを Secret Manager から取得して入れる
LOG_LEVEL=debug
# .gitignore
.env
.env.local
!.env.example

.env.example の役割は**「何を設定する必要があるか」の一覧**です。 値は空でかまいません。新しい環境変数を追加したら、必ず .env.example にも追加してください。 これを忘れると、他の人が clone した時に原因不明の起動失敗を踏みます。

秘密の値をコミットしたら、消しても手遅れ

git rm して commit しても、過去のコミットには残ります。 公開リポジトリなら、push した時点で外部のスキャナに拾われていると考えてください。

正しい対応は「履歴から消す」ではなく、その鍵を無効化して新しい鍵を発行する(ローテーション)ことです。 履歴の書き換えは、それとは別に、チームと相談してから行います。

やってしまったら隠さずすぐ報告してください。時間が経つほど被害が広がります。

設定が読めていない時の確認方法

「環境変数を設定したのに反映されない」は頻出です。上から順に確認します。

# 1. ファイルは存在するか(.env.local と間違えていないか)
ls -la | grep env
 
# 2. Compose が実際に何を渡すつもりか(変数展開後の最終形が見える)
docker compose config
 
# 3. コンテナの中に届いているか
docker compose exec api env | grep DATABASE_URL
 
# 4. アプリのプロセスが実際に読んだ値は何か
#    起動時に接続先をログへ出しておくと、この確認が一瞬で終わる
docker compose logs api | head -20

よくある原因は次のとおりです。

症状原因
値が空になる.env を作ったが docker compose up を再実行していない
古い値のままrestart ではなく up -d が必要(環境変数の変更は再作成が要る)
ローカルだけ動くアプリをホストで起動しており .env の db が解決できない
値の末尾に空白が入るKEY=value # コメント と書いた。# 以降も値として読む実装がある
クォートが値に入るKEY="value" の " ごと読む実装がある

なお、アプリの起動直後に接続先・ログレベル・環境名を1行ログへ出しておくと、 この手の調査がほぼ不要になります。

starting api env=development db_host=db log_level=debug port=8080

ただしパスワードやトークンは絶対に出さないでください。 出すのは「どこに繋ぎにいくか」だけで十分です。

エディタと LSP

効くと読む速度が変わる機能

エディタが賢く見えるのは、裏で LSP(Language Server Protocol) サーバーが動いているからです。 gopls(Go)、typescript-language-server(TypeScript)といったプログラムが、 コードを解析して結果をエディタに返しています。

機能何が起きるかこれが無いと
補完メソッド名が候補に出るドキュメントを毎回開く
定義ジャンプ(F12)関数の実装へ飛ぶgrep で名前を探す
参照検索(Shift+F12)どこから呼ばれているかを列挙影響範囲が読めず、消せない
型情報のホバー変数の型がその場で見える頭の中で型を追う
リネーム(F2)呼び出し側もまとめて改名手で直して漏らす

新人と経験者で読解速度に差がつく理由の一部は、純粋にこれです。 知らないコードを読む時、定義ジャンプと参照検索があるかどうかで探索の効率が変わります。

効かない時に確認すること

補完も定義ジャンプも死んでいる、という状態は珍しくありません。順に確認します。

  1. そもそも言語サーバーが起動しているか VS Code なら「出力」パネルで gopls や TypeScript のログを選び、エラーが出ていないか見ます
  2. プロジェクトのルートを開いているか go.mod や tsconfig.json があるディレクトリを開く必要があります。 その1つ上の親ディレクトリや、逆にサブディレクトリだけを開いていると、モジュールを解決できません
  3. 依存がインストールされているか node_modules が無ければ TypeScript は型を解決できません。npm install / go mod download が先です
  4. 言語のバージョンが合っているか mise で切り替えた Node を、エディタが見つけられていないことがあります。 エディタをターミナルから起動する(code .)と PATH を引き継げます
  5. キャッシュを捨てる VS Code なら TypeScript: Restart TS Server、Go なら gopls: Restart Language Server

モノレポでは「どこを開くか」が効く

サービスが複数ある大きなリポジトリ(モノレポ)では、ルートを開いただけでは解決に失敗することがあります。

  • Go なら go.work を置いて複数モジュールを1つのワークスペースにまとめる
  • TypeScript なら各パッケージの tsconfig.json が参照関係を正しく張っているか確認する

「エディタが遅い・重い」の原因が、実は言語サーバーが巨大な範囲を舐めていることだった、 というのもよくある話です。

デバッガ — print から卒業する

console.log や fmt.Println を挟んで動かし、出力を見て、また挟む。 最初はこれで足ります。ただし、次の状況で急に効率が落ちます。

  • どこで壊れているか分からない — 挟む場所が分からないので、当てずっぽうで20箇所に入れる
  • オブジェクトが大きい — 全部出すとログが読めない。必要なフィールドを事前に決められない
  • ループの途中で1回だけおかしい — 100回ぶんの出力から目で探すことになる
  • 他人のライブラリの中 — そもそも print を書き込めない

デバッガは「実行を止めて、その瞬間の全変数を見る」道具です。 事前に何を見るか決めなくてよいのが本質的な違いです。

用語

操作意味
ブレークポイント「この行に来たら止まれ」という印
ステップオーバー次の行へ進む(関数呼び出しの中には入らない)
ステップイン呼び出している関数の中へ入る
ステップアウト今の関数を抜けて呼び出し元へ戻る
コールスタック「ここに来るまでにどの関数を通ったか」の一覧
ウォッチ式特定の式の値を止まるたびに評価して表示する
条件付きブレークポイントid === 42 の時だけ止まる

条件付きブレークポイントを知っているかどうかで、ループの調査時間が変わります。 1000件のうち1件だけ壊れているなら、その条件を書いて止めるのが最短です。

VS Code の launch.json

.vscode/launch.json に設定を置くと、F5 で起動できます。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Node: サーバーを起動して止める",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/main.ts",
      "runtimeArgs": ["--import", "tsx"],
      "envFile": "${workspaceFolder}/.env",
      "console": "integratedTerminal",
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "name": "Node: 起動済みプロセスに接続",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "restart": true,
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app"
    },
    {
      "name": "Go: 現在のパッケージを起動",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${workspaceFolder}/cmd/api",
      "env": { "DATABASE_URL": "postgres://app:password@localhost:5432/app_development" }
    },
    {
      "name": "Go: 今開いているテストをデバッグ",
      "type": "go",
      "request": "launch",
      "mode": "test",
      "program": "${fileDirname}"
    }
  ]
}

押さえるべきは request の2種類です。

  • launch — デバッガがプロセスを起動する。ホストで動かす時はこちら
  • attach — すでに動いているプロセスに後から繋ぐ。コンテナの中で動いているアプリはこちら

コンテナの中のアプリに繋ぐには、アプリ側をデバッグ待ち受けで起動し、そのポートを公開します。

  api:
    command: node --inspect=0.0.0.0:9229 dist/main.js
    ports:
      - "8080:8080"
      - "9229:9229"     # デバッガ用のポートも公開する

--inspect=0.0.0.0:9229 の 0.0.0.0 が重要です。既定の 127.0.0.1 のままだと、 コンテナの中からしか繋げず、ホストの VS Code からは接続できません。ポート公開と同じ話です。

Go の場合は delve を使います。

dlv debug ./cmd/api --headless --listen=:2345 --api-version=2
それでも print デバッグが正しい場面はあります

デバッガが常に上位互換なわけではありません。次の場合は print のほうが速く、確実です。

  • タイミングに依存するバグ — 止めると再現しなくなる(並行処理、レースコンディション)
  • 本番やステージング — プロセスを止められない。構造化ログを足して再デプロイするしかない
  • 多数のリクエストにまたがる問題 — 1回止めても分からない。ログを集計して傾向を見る
  • とりあえず1行だけ見たい — 5秒で終わるなら print でよい

判断基準は単純です。「どこが怪しいか分かっている」なら print、「分からない」ならデバッガです。

compose.yaml でアプリと Postgres を立ち上げ、.env に DATABASE_URL=postgres://app:password@localhost:5432/app_development と書いたところ、アプリのログに dial tcp 127.0.0.1:5432: connect: connection refused と出ました。原因は何ですか。

「自分の環境だけ動かない」の切り分け

同僚の手元では動く、CI でも通る、でも自分の Mac だけ動かない。 この状況は必ず来ます。焦って再インストールを繰り返す前に、差分を潰す順番を決めておきます。

手順

# 1. 本当に自分だけか確認する(同僚1人に同じコマンドを試してもらう)
 
# 2. 差分がありそうな場所を、上から順に見る
git status                    # コミットしていない変更が残っていないか
git log --oneline -3          # 同じコミットを見ているか
mise current                  # 言語バージョンは合っているか
docker compose config         # 設定の最終形は同僚と同じか
diff <(sort .env) <(sort .env.example)   # 環境変数の項目に漏れがないか
 
# 3. 生成物とキャッシュを疑う
ls node_modules | wc -l       # 依存は入っているか
git status --ignored          # 追跡外の残骸が邪魔していないか

差分が生まれる場所は、実はそう多くありません。

疑う順差分の正体確認コマンド
1言語・ツールのバージョンmise current / node -v / go version
2環境変数(項目の漏れ、値の誤り)docker compose config / exec env
3依存の状態(lockfile と実体のずれ)npm ci / go mod download
4ビルド生成物・キャッシュの残骸dist/ .next/ tmp/ を消す
5コンテナのイメージが古いdocker compose build --no-cache
6DB のスキーマが中途半端マイグレーションの適用状況
7OS 差(BSD と GNU、ファイル名の大小、CPU アーキテクチャ)マシンとシェル参照

3番目について1つ注意です。npm install は条件次第で lockfile を書き換え、同僚と違うバージョンを入れます。 環境を揃えたい時は npm ci を使ってください。lockfile どおりに、まっさらから入れ直します。 そして package-lock.json に差分が出たら、それは無視するものではなくコミットしてレビューに乗せるものです。

まっさらから作り直す判断

順に潰しても分からない時があります。その時は作り直します。ただし基準を決めておきます。

  • 30分調べて原因の見当がつかない → 作り直す
  • 原因が分かっている → 作り直さない(同じ状態が再現するだけで、時間を捨てることになります)
# コンテナと DB をまっさらに
docker compose down -v
docker compose build --no-cache
docker compose up -d
 
# 依存と生成物をまっさらに
rm -rf node_modules dist .next
npm ci
go clean -cache -modcache   # Go は時間がかかるので最後の手段
 
# 追跡外のファイルを全部消す(危険: 先に -n で確認する)
git clean -xdn              # 消される候補を表示するだけ
git clean -xdf              # 実際に消す
git clean -xdf の前に必ず -n

-x は .gitignore されたファイルも対象にします。つまり .env も消えます。 API キーを書いていたなら、再取得からやり直しです。

必ず -n(dry run)で一覧を見てから実行してください。 消えて困るものは、先にどこかへコピーしておきます。

作り直した後は、何が原因だったか分からないままになりがちです。 それでも構いませんが、git clean の直前の状態で気づいたこと(残っていた生成物、古いイメージ)を チームのチャットに一言残しておくと、次に同じことが起きた人が助かります。

セットアップ手順書を疑う

READMEの手順どおりにやったのに動かない。これはあなたの落ち度ではありません。

手順書は必ず腐ります。理由は構造的なものです。

  • 依存を追加した人が README を直さない(動いている本人は困らない)
  • 言語やツールのバージョンが上がって、コマンド名やオプションが変わる
  • 書いた人のマシンには既に入っていたものが、手順から抜け落ちている
  • Apple Silicon への移行のような、書いた当時に存在しなかった前提が増える

だから、詰まった時の正しい振る舞いはこうです。

  1. エラーメッセージをそのまま読む(この教科書の使い方の話です。推測の前に読む)
  2. 15〜30分で解決しなければ聞く。その際に「どこまで進んで、何が出たか」を貼る
  3. 解決したら、その場で README を直して PR を出す

3つ目が重要です。環境構築で詰まった直後が、手順書を直せる唯一のタイミングです。 翌日には「なぜ詰まったか」を忘れています。

新人の最初の PR に最適です

セットアップ手順の修正は、新しく入った人にしか書けません。 既にできている人には、何が足りないかが見えないからです。

docs: README のセットアップ手順を修正

- mise install の前に mise activate が必要な旨を追記
- Node のバージョン指定が .tool-versions と食い違っていたので合わせた
- Apple Silicon で必要になる platform 指定を追記

小さな PR ですが、次に入る人の初日を丸一日短縮します。 レビューの流れを覚える練習としても手頃です。

手順書より強いのは「実行できる手順」

README に10ステップ書くより、make setup の1行にまとめるほうが腐りません。 文章は実行されませんが、スクリプトは実行されるので、壊れればすぐ分かります。

setup:
	mise install
	npm ci
	cp -n .env.example .env
	docker compose up -d
	npm run db:migrate

余力があれば、README を直すついでにこの形へ寄せることも検討してください。

実務の落とし穴まとめ

  1. コンテナ内の localhost — 自分自身を指す。DB へは Compose のサービス名で繋ぐ
  2. depends_on だけで待った気になる — DB が受付可能になるまでは待たない。healthcheck かリトライを
  3. down -v を癖にする — ローカル DB のデータが消える。まず logs -f
  4. .env をコミットする — 履歴に残る。消すのではなく鍵をローテーションして即報告
  5. .env.example を更新し忘れる — 他の人が clone した時に原因不明の起動失敗を踏む
  6. git clean -xdf をいきなり実行 — .env まで消える。必ず -n で確認してから
  7. npm install で環境を揃えたつもり — lockfile が書き換わる。揃えるなら npm ci
  8. 127.0.0.1 にバインドしたままコンテナ化 — ポートを公開しても外から届かない
  9. 原因が分かっているのに作り直す — 同じ状態が再現するだけ。時間だけが減る

まとめ

  • 言語バージョンは口頭で揃えず、.tool-versions に宣言してコミットする
  • 依存サービスは Docker Compose で立てる。壊れたら作り直せるのが最大の利点
  • コンテナの中と外は別のネットワーク。localhost はコンテナ自身。サービス名で繋ぐ
  • .env はコミットしない、.env.example はコミットする
  • 設定が読めていない時は、docker compose config → exec env → 起動ログの順で確認する
  • 定義ジャンプと参照検索が効くだけでコードを読む速度が変わる。効かない時はルート・依存・バージョンを疑う
  • デバッガは「事前に何を見るか決めなくてよい」のが本質。コンテナ内のアプリには attach で繋ぐ
  • print デバッグも有効。怪しい場所が分かっているなら print、分からないならデバッガ
  • 動かない時は差分を上から潰す(バージョン → 環境変数 → 依存 → 生成物 → イメージ → DB → OS)
  • 手順書は腐る。詰まった直後に直して PR を出すのが正しい振る舞い

公式ドキュメント

迷ったら一次情報に戻ってください。

対象リンク
Docker Compose ドキュメントhttps://docs.docker.com/compose/
mise(言語バージョン管理)https://mise.jdx.dev/
VS Code ドキュメントhttps://code.visualstudio.com/docs
Dev Containershttps://containers.dev/

章末問題

同僚の環境では動くアプリが、自分の Mac だけ起動に失敗します。最初にやるべきことはどれですか。

新しい環境変数 STRIPE_SECRET_KEY をアプリに追加しました。リポジトリに対して行うべき正しい対応はどれですか。

次の章からは第2部です。手元で動く環境ができたので、 そのコードをチームでどう進め、どうレビューして本番まで運ぶのか、という仕事の流れに移ります。

読み終わったら記録しておくと、目次で進み具合が分かります。