はじめに
AWS App RunnerでNext.js(standaloneモード)をデプロイしようとしたところ、ヘルスチェックが通らずサービス作成に失敗する問題に遭遇しました。
ローカルのDockerでは正常に動作するのに、App Runnerでは失敗する。この謎を「なぜなぜ分析」で解明し、解決した記録です。
環境
- Next.js 16.0.7(standalone出力モード)
- Node.js 20 Alpine
- AWS App Runner(東京リージョン)
症状
App Runnerコンソールでの表示
作成に失敗 three-sisters-portal
Health check failed on protocol TCP [Port: '8080']
CloudWatch Logsでのアプリケーションログ
▲ Next.js 16.0.7
- Local: http://ip-10-0-104-219.ap-northeast-1.compute.internal:8080
- Network: http://ip-10-0-104-219.ap-northeast-1.compute.internal:8080
✓ Starting...
✓ Ready in 151ms
一見、正常に起動しているように見えます。
ローカルDockerでのログ
▲ Next.js 16.0.7
- Local: http://localhost:8080
- Network: http://0.0.0.0:8080
✓ Starting...
✓ Ready in 63ms
違いに気づきましたか?
問題の原因:なぜなぜ分析
5つのなぜ
-
なぜヘルスチェックが失敗?
→ ポート8080へのTCP接続ができない -
なぜ接続できない?
→ Next.jsが0.0.0.0ではなく、内部ホスト名(ip-10-0-104-219...)にバインドしている -
なぜ内部ホスト名にバインド?
→ Next.js standaloneのserver.jsはHOSTNAME環境変数を参照してバインドアドレスを決定する -
なぜHOSTNAMEが内部ホスト名になっている?
→ App Runnerがコンテナ起動時にHOSTNAME環境変数を自動設定している -
根本原因
→ App Runnerの仕様。コンテナオーケストレーターは一般的にHOSTNAMEを内部ネットワークのホスト名で設定する
Dockerfileの罠
最初のDockerfile:
ENV HOSTNAME="0.0.0.0"
ENV PORT=8080
CMD ["node", "server.js"]
ENVでHOSTNAMEを設定しても、App Runnerが実行時に上書きするため意味がありません。
解決策の比較
| 方法 | 確実性 | 理由 |
|---|---|---|
Dockerfile内でENV HOSTNAME="0.0.0.0"
|
x | App Runnerが実行時に上書き |
App Runner環境変数でHOSTNAME=0.0.0.0設定 |
△ | App Runnerが後から上書きする可能性 |
| 起動スクリプトで強制設定 | ◎ | CMD実行時に再設定するため確実 |
解決策:CMDでHOSTNAMEを強制設定
# NG: App Runnerに上書きされる
CMD ["node", "server.js"]
# OK: シェル経由でHOSTNAMEを強制設定
CMD ["sh", "-c", "HOSTNAME=0.0.0.0 node server.js"]
なぜこれで動くのか?
- コンテナ起動時、App Runnerが
HOSTNAMEを内部ホスト名に設定 - CMDが実行される
-
sh -cにより、その場でHOSTNAME=0.0.0.0が環境変数として設定 -
node server.jsは新しいHOSTNAME=0.0.0.0を参照 - Next.jsが
0.0.0.0:8080にバインド - ヘルスチェック成功!
完全なDockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile || pnpm i
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 8080
# ポイント: HOSTNAMEを強制設定してからnode起動
CMD ["sh", "-c", "HOSTNAME=0.0.0.0 node server.js"]
next.config.mjsの設定
standalone出力を有効にするために必要:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
export default nextConfig
App Runner設定
{
"instanceConfiguration": {
"cpu": "1024",
"memory": "2048"
},
"healthCheckConfiguration": {
"protocol": "TCP",
"port": "8080",
"interval": 10,
"timeout": 5,
"healthyThreshold": 1,
"unhealthyThreshold": 5
}
}
教訓
- Next.js standaloneモードは
HOSTNAME環境変数に依存する - コンテナオーケストレーター(App Runner, ECS, K8s等)は
HOSTNAMEを自動設定することがある - Dockerfileの
ENVは実行時に上書きされる可能性がある - 確実な方法は、CMDでシェル経由で環境変数を設定すること
他のプラットフォームでも要注意
この問題はApp Runner固有ではありません。以下のプラットフォームでも同様の注意が必要です:
- AWS ECS / Fargate
- Google Cloud Run
- Azure Container Apps
- Kubernetes
コンテナオーケストレーターを使う場合は、HOSTNAME環境変数の挙動を確認しましょう。
まとめ
「ローカルでは動くのに本番で動かない」という典型的なハマりパターンでしたが、なぜなぜ分析で根本原因を特定できました。
ログのNetwork: http://0.0.0.0:8080とNetwork: http://ip-10-0-xxx...:8080の違いに気づけたのが解決の鍵でした。
同じ問題でハマっている方の参考になれば幸いです。