Docker開発環境の「待ち時間」をゼロにする:Node.js現場の泥臭い最適化術

Docker開発環境の「待ち時間」をゼロにする:Node.js現場の泥臭い最適化術

「コーヒーを淹れて戻ってきたら、まだコンテナが立ち上がっていない」。
この光景を見て、君は何を感じる? 多くのエンジニアが「Dockerだから遅いのは仕方ない」と諦めているが、それは完全な間違いだ。遅いのはDockerのせいじゃない、君のDockerfileが「ゴミ」を溜め込んでいるからだ。

かつての俺は、ビルドのたびに1GB超えのイメージを生成し、`node_modules`の同期不整合で「なぜかローカルでだけ動かない」という地獄のデバッグに週の半分を溶かしていた。あの焦燥感と、無駄に過ぎ去る数分間。あれはエンジニアにとっての毒だ。

本稿では、Node.js/TypeScript環境を前提に、開発スピードを劇的に引き上げるための「泥臭い最適化」を共有する。


1. マルチステージビルド:なぜ `alpine` を選ぶのか

多くの初心者がやりがちなのが、本番環境と開発環境を同一視した巨大なDockerfileだ。`node_modules`やビルドツールを全て詰め込み、「イメージサイズが1GB超えました」などと笑えない冗談を言っているなら、今すぐやめるべきだ。

# ビルドステージ
FROM node:18-alpine AS builder
WORKDIR /app
# 依存関係のインストール
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 実行ステージ
FROM node:18-alpine
# セキュリティと軽量化のためにalpineを選択
# 注意: alpineはmusl libcを採用しているため、特定のネイティブアドオンが動かないリスクがある
# その場合は debian-slim への切り替えを検討せよ
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]

なぜ `alpine` なのか?
単にサイズが小さいからではない。攻撃対象領域(Attack Surface)を最小化できるからだ。ただし、alpineは標準のglibcではなく`musl`を採用しているため、C++で書かれたネイティブモジュールが死ぬことがある。「軽量化」と「互換性」のトレードオフを理解できないエンジニアは、現場で致命的な地雷を踏む。


2. 匿名ボリューム:ホストOSの「汚染」を防ぐ

これが最も多くハマる落とし穴だ。`node_modules`をホストからマウントすると、ホストOS(macOS/Windows)でビルドされたバイナリがコンテナ内のLinux環境に持ち込まれる。

ABI(Application Binary Interface)の不一致により、コンテナ内で動かそうとした瞬間に「`Illegal instruction`」や「`Module not found`」で爆死する。これを防ぐには、`node_modules`を匿名ボリュームで隔離するのが鉄則だ。

services:
  app:
    volumes:
      - .:/app
      - /app/node_modules # ここでホストの汚染を遮断する

これだけで、「俺のPCでは動くんですけどね」という恥ずかしい言い訳を一生封印できる。


3. DBマイグレーション自動化:`sleep 10` はもう卒業しろ

コンテナ立ち上げ後に手動でマイグレーション? 昭和かよ。`entrypoint.sh` をハックして、インフラの準備が整った瞬間にアプリケーションを叩き起こすのがプロの所業だ。

多くの現場でありがちな「DBが起動するまでとりあえず`sleep 10`」という実装は、DBの起動が遅い環境では無力だ。`netcat (nc)` を使い、確実にポートが開くまで待機させる。

#!/bin/sh
set -e

# netcatがない環境に備え、必要ならインストールする
# apk add --no-cache netcat-openbsd

until nc -z -v -w30 db 5432; do
  echo "Waiting for database connection..."
  sleep 2
done

echo "Database is ready! Running migrations..."
npm run typeorm migration:run

exec "$@"

`docker-compose.yml` 側の設定も忘れずに:

services:
  app:
    entrypoint: ["/bin/sh", "/app/entrypoint.sh", "npm", "run", "dev"]
    depends_on:
      - db


4. 最後に:環境は「使い捨て」であれ

俺が数々のプロジェクトを渡り歩いて確信したことがある。「環境の再現性」こそが最強の武器だ。

環境が壊れたときに「手動で修正」しようとするな。コンテナを破棄し、再構築して、マイグレーションとシードデータを流し込む。それが一瞬で終わる環境こそが、君の生産性を最大化する。

Dockerは魔法じゃない。ただの道具だ。ビルドが遅いのはツールのせいじゃない。君がその道具の特性を理解せず、ただ「動けばいい」という甘えでDockerfileを放置しているからだ。

今すぐ`Dockerfile`と`docker-compose.yml`を見直せ。エンジニアとしてのプライドを、設定ファイルの中に刻み込め。