Build Stage

深夜2時、コーヒー4杯目のカフェインで心臓がバクバクしてる。本番環境のデプロイボタンを押す瞬間のあの独特の緊張感、あれって何年経っても慣れないよな。

今日は、手動デプロイという名の「修行」から卒業し、DockerとGitHub Actionsで泥臭くも堅牢なパイプラインを組んだ話を共有する。「自動化は魔法じゃない、ただの積み重ねだ」ってことを、俺の失敗と共に持ち帰ってくれ。


1. マルチステージビルド:イメージの「贅肉」を削ぎ落とす

適当に書いたDockerfileで1GB超えのイメージをECRにプッシュする……あの待ち時間、人生の無駄遣いにもほどがある。特に深夜の回線でアレをやると心が折れる。

解決策はマルチステージビルド一択だ。「ビルド環境」と「実行環境」を物理的に分離する。

# Build Stage
FROM node:18-slim AS builder
WORKDIR /app
COPY package*.json package-lock.json ./
RUN npm ci  # npm installより高速で再現性が高い
COPY . .
RUN npm run build && npm prune --production # 開発用依存関係を容赦なく切り捨てる

# Production Stage
FROM node:18-slim
WORKDIR /app
# ビルド済みの成果物と、必要最小限のnode_modulesだけをコピー
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]

ポイントは `npm ci` と `npm prune --production` だ。これだけでイメージサイズは800MBから150MBまで落ちる。「動けばいい」ではなく「最小限で動かす」。これがコンテナ時代のマナーだ。


2. 環境変数の管理:沼にはまるな

`.env`をGitに乗せてGitHubに公開し、秒速でマイニングサーバーにされた苦い思い出がある。あの時の絶望感と言ったら……。

Docker Composeで環境変数を扱うなら、潔く `.env` を活用しろ。`docker-compose.yml` と同じ階層に `.env` を置けば、Composeが勝手に読み込んでくれる。

# docker-compose.yml
services:
  app:
    image: my-app:${TAG:-latest} # デフォルト値の設定
    env_file: .env
    environment:
      - NODE_ENV=production

教訓: 複雑なSecrets管理に手を出してSwarm専用の記法で詰まるより、環境ごとに `.env.prod` や `.env.dev` を用意し、起動時に `docker-compose --env-file .env.prod up -d` と叩く方が圧倒的に平和だ。「シンプルが最強」。これに尽きる。


3. GitHub Actions:深夜のデプロイを「無」にする

サーバーにSSHして `git pull` してビルド……そんな作業はもう古い。GitHub Actionsで、マージした瞬間にデプロイが完了するフローを作る。

重要なのは、サーバー上でビルドしないこと。サーバーに負荷をかけるな。GitHub上でビルドし、Docker HubにPushし、サーバーはそれをPullするだけにする。

# .github/workflows/deploy.yml
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build and Push
        run: |
          docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
          docker build -t my-app:latest .
          docker push my-app:latest

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          script: |
            docker pull my-app:latest
            docker compose up -d --force-recreate # ダウンタイムを最小化

これで、コードをPushして寝れば、朝には最新版が動いている。最高だろ?


4. キャッシュ地獄からの脱出

今回一番ハマったのが「ビルドキャッシュ」だ。GitHub Actionsで `cache-from` を適当に使ったら、古いコードがコンテナに残る怪奇現象が起きた。

解決策: Actionsのキャッシュを正しく制御しろ。

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

- name: Build and push
  uses: docker/build-push-action@v4
  with:
    push: true
    tags: my-app:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

`mode=max` を指定しないと、中間レイヤーのキャッシュが不完全で、結局ビルドが遅くなる。便利機能に頼る時は、その裏側の仕様まで理解しておかないと、深夜3時に顧客からの「画面が変わってないぞ」というチャットで死ぬことになる。


結論:泥臭く、しかしスマートに

自動化は正義だけど、構築過程で一度は死ぬ。でも、その死を乗り越えた先にある「寝てる間にデプロイが終わってる」快感は、エンジニアにしか分からない麻薬だ。

結局、一番のデバッグツールは「深夜の集中力」と「諦めない心」だ。……さて、コーヒー、もう一杯入れてくるわ。

自動化で楽をするために、今日も泥臭くコンテナを叩く。