
ECS上でNext.jsの環境変数を参照する際にハマったポイントをまとめてみた。
はじめに
みなさんこんにちは、クラウド事業本部コンサルティング部の浅野です。
ECSでNext.jsをホスティングしたことがなかったため、やってみたところ環境変数の参照周りでいくつかハマったのでまとめてみました。
注意点として下記のハマりポイントはECSに限った話ではなく、たまたま今回ECS上で起きたというだけです。
動作環境
メインフレームワーク
- Next.js: 15.5.2
- ビルドオプション: Turbopack
- React: 19.1.0
Docker環境
- ベースイメージ: node:20-alpine
- 実行方式: Server mode (非 standalone)
① ブラウザ環境変数がビルド時にインライン化される
Next.js で取り扱う環境変数には以下の2種類があります。
-
- サーバー側でのみ取り扱い可能な環境変数: 「NEXT_PUBLIC_」接頭辞がついていない環境変数の場合はNode.js環境からのみ利用可能であり、ブラウザからアクセスできません。
-
- ブラウザ用環境変数: 「NEXT_PUBLIC_」接頭辞を含む環境変数はサーバー側からもブラウザ側からも使用可能です。ユーザーが値を確認しようと思えば可能なので機密性の高い情報は含めないように注意。
- ブラウザ用環境変数 の場合はビルド時に値をクライアントに配信するJSバンドルで動的値(process.env.NEXT_PUBLIC_HOGEHOGE)から静的値へインライン化するため、「npm run build」の段階で環境変数を渡してあげる必要がありました。なのでECSの環境変数に「NEXT_PUBLIC_HOGEHOGE」と設定してもDockerファイルにてビルドする段階でその情報がまだないので、コード上ではundefined判定をくらいます。
以下は具体例です。
Dockerfile内でビルドするときに環境変数を渡していません。
なのでCDKで以下のようにタスク定義に環境変数を設定してもビルド時に静的値に変わっているので意味がありません。
# ベースイメージ
FROM node:20-alpine
RUN apk add --no-cache curl netcat-openbsd bind-tools
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 80
# 起動
CMD ["npm", "start"]
// タスク定義
const frontendTaskDef = new ecs.FargateTaskDefinition(this, 'FrontendTaskDef', {
memoryLimitMiB: 512,
cpu: 256,
executionRole: taskExecutionRole,
taskRole: taskRole
});
frontendTaskDef.addContainer('frontend-container', {
image: ecs.ContainerImage.fromRegistry(`${cdk.Aws.ACCOUNT_ID}.dkr.ecr.${cdk.Aws.REGION}.amazonaws.com/${props.frontendRepo}:latest`),
memoryLimitMiB: 512,
portMappings: [
{
containerPort: 80,
protocol: ecs.Protocol.TCP
}
],
environment: {
'NODE_ENV': 'production',
'PORT': '80',
'NEXT_PUBLIC_CLIENT_URL': 'https://hogehoge.com' // ブラウザ環境変数を定義
},
logging: ecs.LogDrivers.awsLogs({
logGroup: props.frontendLogGroup,
streamPrefix: 'frontend'
})
});
コード上で以下のように記述しても{process.env.NEXT_PUBLIC_CLIENT_URL}の値は空です。
'use client'
export default function ClientPage() {
return (
<div className="mt-8 p-4 bg-green-50 border border-green-200 rounded-lg">
<h3 className="text-lg font-semibold text-green-800 mb-2">クライアント側環境変数テスト</h3>
<div className="bg-green-100 p-3 rounded">
<p className="text-green-700 mb-1"><strong>NEXT_PUBLIC_CLIENT_URL:</strong></p>
<p className="text-green-800 font-mono text-sm">
{process.env.NEXT_PUBLIC_CLIENT_URL}
</p>
</div>
</div>
)
}
実際の表示
他の記事や公式ドキュメントにもインライン化に関する記載がありました。きちんと確認することが肝心ですね。
公式ドキュメント「環境変数」
対応策
Dockerfile内で環境変数を渡す
様々な対応策がありますがシンプルにDockerfile内で.env.production
に値を渡してビルドすることで適切な値にインライン化してくれます。
その他ssm parameter store等から取得してきて渡してあげても良いと思います。
# ビルド時に .env.production を作成(例)
RUN echo "NEXT_PUBLIC_CLIENT_URL=https://hogehoge.com" >> .env.production
変数化して動的検索にする
公式ドキュメントの記述をそのまま引っ張りますが動的検索値はビルド時のインラインから除外されるので、コード上で以下のように変数化を工夫すればビルド時に値を渡す必要がなくなりランタイム上で評価されます。
// 変数を使用しているためこれはインライン化されません。
const varName = 'NEXT_PUBLIC_ANALYTICS_ID'
setupAnalyticsService(process.env[varName])
// 変数を使用しているためこれはインライン化されません。
const env = process.env
setupAnalyticsService(env.NEXT_PUBLIC_ANALYTICS_ID)
② Server Componentsの「Static Rendering」モードだとサーバー側環境変数もビルド時にインライン化される
上記の現象は「NEXT_PUBLIC_」を含まない環境変数でも起こる可能性があります。
SSR等でServer Componentsを使用する際のデフォルトのレンダリングモードは「Static Rendering」になっており、以下のような記述はビルド時に静的HTMLファイルとしてまとめられるので意味がありません。
// Static Rendering時の例(ビルド時にインライン化される)
'use server'
export default function StaticServerComponent() {
// ビルド時に値がインライン化
const apiUrl = process.env.API_BASE_URL // ビルド時: undefined
return (
<div>
<p>API URL: {apiUrl}</p> {/* 常に "undefined" */}
</div>
)
}
公式ドキュメント「Server Components」
対応策
Dynamic Renderingに変更する
環境変数をServer Componentsで使用する場合は「Static Rendering」ではなく「Dynamic Rendering」に明示的に切り替えてあげましょう。
以下のような動的関数を用いることで強制的に切り替えることが可能です。
- cookies()
- headers()
- noStore()
例
// Server Component を Dynamic Rendering に強制
import { unstable_noStore as noStore } from 'next/cache'
// Server Component - 直接Django API呼び出し
export default async function ServerComponentTest() {
// Dynamic Rendering を強制(ビルド時実行を防ぐ)
noStore()
// 環境変数がリクエスト時にランタイム上で処理されるようになる
const serverComponentApiUrl = process.env.API_BASE_URL
.
.
.
}
Dynamic Renderingに変更すると完全な静的ファイルではなくなるので多少キャッシュ効率は落ちますが、各リクエスト時に更新が必要なデータのみを更新しそれ以外はキャッシュを活かせるのでServer Componentsとして使用するメリットは活かせます。
最後に
Next.js はCSR, SSR, ISR等によって各コードが処理されるタイミングが異なるので、各処理順や仕様をきちんと把握していないと今回のような問題にハマります。
今回のハマりポイントのキモである「ビルド時にインライン化される」というのは環境変数だけに限った話ではなく、動的値を出したいのに常に固定値しか出ない。みたいな問題に繋がる可能性もあると思いました。
仕様を把握すると、そらそうだわとなりましたが、意外と感覚で設定を進めるとなぜ表示されないんだ?みたいな気持ちになったので公式ドキュメントをしっかりとチェックしましょう!今回は以上です。