docker-compose 開発環境が「DBにつながらない」を一発で直す設計図(app+db+redis)
compose で app+postgres+redis を一括起動。depends_on と healthcheck の正しい組み方、ホットリロード、localhost 接続ミスの切り分けまで、僕が踏んだ失敗込みで解説。
「docker compose up したのに、アプリが起動直後に落ちる」
ログを見ると ECONNREFUSED postgres:5432。でも docker compose ps を叩くと postgres は確かに Up になっている。起動してるのに、つながらない。この矛盾の前で、僕は最初の頃30分くらい固まりました。
原因はあっけないものでした。Compose の Up は「コンテナのプロセスが立ち上がった」という意味でしかなくて、「Postgres が接続を受け付けられる状態になった」とは別物だったんです。家のブレーカーは入ったけど、まだエアコンが冷風を出すまで数十秒かかる、あの感じ。
ここを depends_on と healthcheck でちゃんと繋ぐと、嘘みたいにピタッと安定します。今日はそれを、僕が実際に踏んだ失敗込みで、コピペで動く形まで持っていきます。
この記事の要点
- Compose の
Upは「起動」止まり。「接続できる」状態はhealthcheckで別に判定する。 depends_onにcondition: service_healthyを付けると、DB が healthy になるまで app の起動を待たせられる。これが「起動直後に落ちる」の特効薬。- コンテナ内から DB に繋ぐホスト名は
localhostではなくサービス名(postgres、redis)。localhostは自分自身を指すので必ず失敗する。 - ホットリロードは「ソースは bind mount」「
node_modulesは named volume で隔離」の二段構えにする。 - 開発の
compose.yamlをそのまま本番設計図にしない。本番は監視・バックアップ・シークレット・公開範囲を別枠でレビューする。
そもそも Compose で何が嬉しいのか
Compose を一言でいうと、**「ローカル開発環境の設計図を1枚の YAML に書いておく道具」**です。アプリ、データベース、Redis、バックグラウンドの worker、これらを docker compose up の一発でまとめて起動できます。
Docker Compose 公式ドキュメントでも、Compose は複数コンテナのアプリを1つの設定ファイルで定義して起動する道具として紹介されています。サービス(services)、ネットワーク(networks)、ボリューム(volumes)、この3つを覚えれば骨格は掴めます。
- services: 動かすコンテナ。app、postgres、redis、worker など。
- networks: コンテナ同士をつなぐ通り道。同じ Compose 内なら自動で1本張られ、サービス名で呼び合える。
- volumes: データの保存場所。DB の中身や
node_modulesをここに逃がす。
僕が小さな Next.js + キュー worker の検証環境で試したとき、効いたのは魔法のプロンプトじゃありませんでした。DB の起動待ち、Redis の永続化、node_modules の扱い、この3つを先に決めただけで、「自分の環境だけ動かない」が激減したんです。
ちなみにファイル名は compose.yaml が今の正式名です。docker-compose.yml でも動きますが、新規なら compose.yaml でいきましょう。昔よく見た先頭の version: "3.8" も、今は不要です(書いても無視されます)。
今回つくる構成
作るのはこの4つです。
| サービス | 役割 | ポイント |
|---|---|---|
app | ブラウザから触る Web アプリ | ソースを bind mount してホットリロード |
worker | メール送信やキュー処理の常駐プロセス | app と同じイメージを使い回す |
postgres | データベース | healthcheck で「接続可能」を判定 |
redis | キャッシュ兼キュー | appendonly で永続化 |
app と worker は「同じイメージ・違うコマンド」で動かします。app は npm run dev、worker は npm run worker:dev、というふうに。両方とも postgres と redis が healthy になってから動き出すように繋ぎます。
サービス定義の細かい仕様は Compose file reference が一次情報です。Claude Code に修正を頼むときも、このページを前提にレビューさせると、古い docker-compose 時代の書き方に引っ張られにくくなります。
どこで使うと効くか
Compose はローカル開発・検証・テストには滅法強い一方、本番運用はまた別の話です。ここを混同しないのが、事故を減らす最初の線引きになります。
| 使いどころ | 向いている理由 | 注意点 |
|---|---|---|
| ローカル開発 | app・DB・Redis・worker を1コマンドで再現 | bind mount の速度差や OS 差は確認する |
| E2E / 統合テスト | テスト用 DB と Redis を短時間で立てられる | CI ではポート衝突とキャッシュを明示 |
| 新メンバーのオンボーディング | .env.example と make setup で手順を固定 | 秘密情報を .env.example に入れない |
| 本番運用 | 小規模な社内ツールなら候補になることも | 監視・復旧・スケール・権限・コストのレビューが必須 |
本番をクラウドで動かすなら、ECS、Kubernetes、Cloud Run、Fly.io、Render などの運用単位と比べることになります。Claude Code に相談するときも「Compose を本番に持ち込む前提でなく、移行先の制約を列挙して」と頼むほうが安全です。
コピペで動く compose.yaml
ここが本題です。Node.js / Next.js 系を想定しています。npm run dev、npm run worker:dev、npm run db:migrate は、あなたの package.json に合わせて読み替えてください。
# compose.yaml
services:
app:
build:
context: .
dockerfile: Dockerfile
target: dev
command: npm run dev -- --hostname 0.0.0.0
ports:
- "3000:3000"
env_file:
- .env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
REDIS_URL: redis://redis:6379
volumes:
- .:/workspace # ソースを丸ごと共有(編集が即反映される)
- node_modules:/workspace/node_modules # 依存だけはコンテナ側を守る
depends_on:
postgres:
condition: service_healthy # postgres が healthy になるまで待つ
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s # 起動直後の失敗は数えない猶予
worker:
build:
context: .
dockerfile: Dockerfile
target: dev
command: npm run worker:dev
env_file:
- .env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
REDIS_URL: redis://redis:6379
volumes:
- .:/workspace
- node_modules:/workspace/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
env_file:
- .env
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init:/docker-entrypoint-initdb.d:ro
healthcheck:
# $$ はコンテナ内の環境変数を読むための書き方(Compose の変数展開ではない)
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"] # 再起動でデータを失わない
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
node_modules:
postgres_data:
redis_data:
このファイルの肝は3つです。
1つ目、接続先は postgres、redis というサービス名で書く。Compose が同じネットワーク内でサービス名を DNS 名として解決してくれるからです。ここを localhost:5432 にすると、コンテナが自分自身を見に行って必ず失敗します(後でハマりどころとして詳しく書きます)。
2つ目、node_modules を named volume に逃がす。.:/workspace でソースを共有すると、ホスト側に node_modules が無い/OS が違うと、コンテナ内の依存ごと上書きされて壊れます。node_modules だけ別ボリュームに分けて、ホストから守るわけです。
3つ目、depends_on に condition: service_healthy を付ける。これで postgres と redis が healthcheck をパスするまで、app と worker は起動を待ちます。冒頭の「Up なのに ECONNREFUSED」は、これで消えます。
healthcheck と depends_on の関係を正しく理解する
ここが一番つまずく所なので、丁寧にいきます。
Compose の公式ガイドはこう言っています。Compose は「コンテナが running になるまで」しか待たず、「ready になるまで」は待たない、と。つまり素の depends_on(条件なし)は「先に起動を始める順番」を決めるだけで、相手が接続を受け付けられるかは見ていません。
そこで healthcheck の出番です。pg_isready のようなコマンドで「いま接続できるか?」を定期的に確認し、成功したらそのコンテナを healthy 扱いにする。そして depends_on 側で condition: service_healthy を指定すると、「相手が healthy になるまで自分は起動しない」という待ち方になります。
整理するとこうです。
| 書き方 | 待つ条件 | 使う場面 |
|---|---|---|
depends_on: [postgres] | postgres が 起動 したら次へ | 順番だけ決めたいとき(接続可否は見ない) |
condition: service_healthy | postgres が healthy になるまで待つ | DB・Redis など「繋がる」まで待ちたいとき |
condition: service_started | 起動したら即次へ(明示版) | healthcheck を付けない軽いサービス |
condition: service_completed_successfully | 一度走って正常終了したら次へ | migration 用の使い捨てコンテナなど |
ひとつ補足を。service_healthy を付けても、アプリ側のリトライをゼロにはしないでください。起動時は守れますが、動作中に DB が一瞬切れること(再起動、ネットワークの瞬断)は別途あります。「起動の待ち=healthcheck、運用中の瞬断=アプリのリトライ」と役割を分けて持っておくと、後で泣きません。
start_period も覚えておくと得します。これは「起動直後の失敗は healthcheck の失敗回数に数えない」猶予期間です。重いアプリだと初回起動に時間がかかるので、ここを 20s などにしておくと、立ち上がりかけで unhealthy 判定される事故を防げます。
ホットリロードを効かせる Dockerfile
ローカル開発では、ソースを編集したら即反映してほしい。これを target: dev のステージと bind mount の組み合わせで実現します。
# Dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /workspace
ENV NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm ci
# 開発用:ソースは COPY しない(Compose の volume で見せる)
FROM base AS dev
EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--hostname", "0.0.0.0"]
# ビルド用:CI と本番イメージで再現するため COPY する
FROM base AS build
COPY . .
RUN npm run build
# 本番用:ビルド成果物だけを薄いイメージに載せる
FROM node:22-alpine AS production
WORKDIR /workspace
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /workspace/.next ./.next
COPY --from=build /workspace/public ./public
USER node
EXPOSE 3000
CMD ["npm", "start"]
ポイントは dev ステージで COPY . . をしないこと。ソースは Compose の .:/workspace でホストから見せるので、イメージに焼き込む必要がないんです。こうしておくと、エディタで保存した瞬間にコンテナ内のファイルも変わり、next dev のホットリロードが効きます。
逆に build ステージでは COPY . . してソースを焼き込みます。CI や本番では「ホストのファイルを見せる」ことができないので、再現性のためにイメージに固める。開発と本番で COPY の有無が逆になる、ここが地味に大事です。
ホットリロードが効かないときの定番チェックも置いておきます。
- ファイル監視が効かない → 一部環境では
CHOKIDAR_USEPOLLING=true(や Next.js なら webpack の polling 設定)を試す。 - 保存しても何も起きない → bind mount のパスがズレていないか(
.:/workspaceとWORKDIRが一致しているか)。 - 依存を入れ直したのに反映されない →
node_modulesは named volume なので、docker compose buildし直すか、ボリュームを作り直す。
.env と環境変数まわり
.env はコミットしません。代わりに .env.example をコミットして、全員が同じ変数名で起動できるようにします。
# .env.example
POSTGRES_USER=app
POSTGRES_PASSWORD=app_password
POSTGRES_DB=app_development
DATABASE_URL=postgresql://app:app_password@postgres:5432/app_development?schema=public
REDIS_URL=redis://redis:6379
NODE_ENV=development
PORT=3000
ここで一度ハマるのが、.env の使われ方が二段あることです。
env_file: .env→ コンテナの中に環境変数として渡される(アプリがprocess.envで読む)。compose.yaml内の${POSTGRES_USER}のような展開 → プロジェクト直下の.envを参照する。
つまり同じ .env が「コンテナに渡す値」と「YAML を組み立てる値」の両方に効きます。だから .env.example の値と compose.yaml の書き方をズラさないことが大事。DATABASE_URL のホスト名を localhost にしてしまう事故も、ここで起きがちです(コンテナから見たら DB は postgres)。
Makefile で「よく打つコマンド」を固定する
毎回長い docker compose ... を打つのは事故のもとなので、Makefile にまとめます。Windows などで make を使わないチームは、同じ中身を package.json の scripts に移してもOKです。
COMPOSE = docker compose --env-file .env -f compose.yaml
.PHONY: setup up up-d down restart logs ps app-shell db-shell redis-cli migrate seed test lint clean
setup:
cp .env.example .env
$(COMPOSE) build
up:
$(COMPOSE) up --build
up-d:
$(COMPOSE) up -d --build
down:
$(COMPOSE) down
restart:
$(COMPOSE) restart app worker
logs:
$(COMPOSE) logs -f app worker postgres redis
ps:
$(COMPOSE) ps
app-shell:
$(COMPOSE) exec app sh
db-shell:
$(COMPOSE) exec postgres psql -U app -d app_development
redis-cli:
$(COMPOSE) exec redis redis-cli
migrate:
$(COMPOSE) run --rm app npm run db:migrate
seed:
$(COMPOSE) run --rm app npm run db:seed
test:
$(COMPOSE) run --rm app npm test
lint:
$(COMPOSE) run --rm app npm run lint
clean:
$(COMPOSE) down --volumes --remove-orphans
exec と run --rm の違いだけ押さえておけば迷いません。exec は起動済みのコンテナに入る操作。run --rm は新しい使い捨てコンテナでコマンドだけ走らせて捨てる操作です。migrate や test を run --rm にしておくと、CI でもローカルでも同じ形で回せて、差が小さくなります。
「動かない」あるあるの切り分け
ここまでが設計図。最後に、僕や周りが実際にハマった「動かない」の切り分けを、症状ベースで並べます。
症状: Up なのに ECONNREFUSED postgres:5432
起動順の問題です。depends_on に condition: service_healthy が付いているか、postgres 側に healthcheck があるかを確認。素の depends_on だけだと「起動した瞬間」に app が走り出して、まだ接続を受け付けていない DB に突っ込みます。
症状: getaddrinfo ENOTFOUND / localhost に繋がらない
接続先ホスト名のミスです。コンテナの中の localhost は「そのコンテナ自身」を指します。DB は別コンテナなので、サービス名 postgres、redis で繋ぎます。.env の DATABASE_URL のホスト名も要チェック。
症状: ソースを直してもホットリロードされない
bind mount かファイル監視の問題です。.:/workspace のパスと WORKDIR が一致しているか、node_modules を named volume で隔離できているかを見ます。監視が効かない環境ではポーリング設定を試す。
症状: スキーマを変えたのに古いデータが残る/「自分だけ壊れる」
named volume の消し忘れです。postgres_data に古い状態が残っていると、新しい init スクリプトや migration の結果がズレます。make clean(= down --volumes)で作り直す。ただしローカルのデータは消えるので、実行前に一呼吸。
症状: port is already allocated
ポートの取り合いです。すでに別の Postgres や別プロジェクトの Compose が 5432 を使っていることが多い。docker compose down で止めるか、ports のホスト側を 5433:5432 などにずらします。
症状: CI では落ちるのにローカルでは通る
キャッシュとポートの差が定番です。CI ではビルドキャッシュが無い前提で、run --rm の形をローカルと揃える。テスト用 DB のポートやサービス名も CI 設定と一致させます。
Claude Code にレビューさせるプロンプト
YAML を人間の目だけで眺めると、localhost 接続、.env 漏れ、volume の上書き、healthcheck 不足を見落とします。観点を固定して Claude Code に渡すと、毎回同じ粒度で確認できて楽です。
Claude Code common workflows でも、日常作業を小さな依頼に分ける考え方が紹介されています。Compose も「作って」より「観点を指定してレビューして」のほうが成果が安定します。
このリポジトリの Docker Compose 構成をレビューしてください。
対象:
- compose.yaml
- Dockerfile
- .env.example
- package.json
- .github/workflows があれば CI 設定
観点:
1. app + postgres + redis + worker がローカルで再現できるか
2. healthcheck と depends_on(condition) の使い方が妥当か
3. named volume と bind mount の使い分けが安全か(node_modules の隔離含む)
4. .env.example に秘密情報が混ざっていないか
5. migrate / seed / test / lint の一回実行コマンドが足りているか
6. CI で失敗しそうなポート、キャッシュ、権限、起動待ちがないか
7. 本番に持ち込む前に別途レビューすべき項目は何か
制約:
- 既存のフレームワークと package manager に合わせる
- 大きなリファクタリングは提案に止める
- 修正する場合は理由と確認コマンドも書く
このプロンプトを CLAUDE.md に近い形で残すと、チームのレビュー基準になります。CLAUDE.md の整え方や、Docker まわりの全体像は Claude CodeとDocker実践ガイド と Claude Code Dev Containerガイド、CI への載せ方は CI/CDセットアップガイド と合わせて読むと、実務に落とし込みやすくなります。
よくある質問
Q. compose.yaml と docker-compose.yml、どっちで書けばいい?
新規なら compose.yaml です。今の正式名で、公式もこちらを優先します。docker-compose.yml も後方互換で動きますが、コマンドも docker-compose(ハイフン)ではなく docker compose(スペース)が現行です。
Q. 先頭の version: "3.8" は書かないとダメ?
今は不要です。Compose Specification では version は obsolete 扱いで、書いても無視されます。消してOKです。
Q. depends_on だけでは起動待ちにならないの?
なりません。素の depends_on は「起動する順番」を決めるだけで、相手が接続可能かは見ません。「繋がるまで待つ」には、相手に healthcheck を付け、condition: service_healthy を指定します。
Q. service_healthy を付ければアプリ側のリトライは不要?
起動時の取りこぼしは防げますが、運用中の瞬断(DB 再起動など)は別物です。起動待ちは healthcheck、動作中の再接続はアプリのリトライ、と二段で持つのが安全です。
Q. ホットリロードが効きません。
まず .:/workspace の bind mount と WORKDIR が一致しているか、node_modules を named volume に分けているかを確認します。それでも監視が効かない環境では、ポーリング(例: CHOKIDAR_USEPOLLING=true)を試してください。
Q. この compose.yaml をそのまま本番に使える?
おすすめしません。開発用にポートを公開していますし、シークレットも平文の .env 前提です。本番は公開範囲、TLS、DB バックアップ、Redis 永続化方針、監視、脆弱性スキャン、権限境界、費用を別枠でレビューします。
実際に試した結果
冒頭の「Up なのに ECONNREFUSED」以来、僕は起動トラブルを「気合いで待つ」のをやめました。代わりにやったのは、postgres と redis に healthcheck を置き、app と worker の depends_on を condition: service_healthy にしただけ。これで「起動直後に落ちる」は手元からほぼ消えました。
node_modules を named volume に逃がしてからは、「自分の OS でだけ依存が壊れる」も起きなくなりました。一方で、スキーマを変えたのに postgres_data を消し忘れて migration の検証結果がズレる、という別の罠は今でもたまに踏みます(make clean 一発で直りますが)。
結論はシンプルです。Compose はローカル開発の再現性を上げる強力な足場。ただし「起動」と「接続できる」は別物だと割り切って、healthcheck で繋ぐ。 そして本番判断はセキュリティ・監視・復旧・費用を別枠でレビューする。この線引きさえ守れば、docker compose up 一発でチーム全員が同じ環境に立てます。
自分のリポジトリに合わせて型を固めるなら ClaudeCodeLab の教材・テンプレート が早道です。チーム導入や本番移行の判断まで一緒に設計したいときは Claude Code 研修・導入相談 もどうぞ。一次情報は Docker Compose 公式 と Compose file reference を当たるのが確実です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。