NEXT_PUBLICがついていない環境変数もインライン化されてしまった話
はじめに
Next.jsのコンテナをECSにデプロイする際に、
タスク定義の環境変数が参照されない問題に遭遇しました。
実際に調べてみると、NEXT_PUBLIC
がついていない環境変数も、
ビルド時の値でインライン化されてしまっていたことがわかりました。
上記の現象が発生した原因について調べた内容について記載しようと思います。
結論
next.config.ts
のenv
オプションを設定していることが原因でした。
具体的には、下記のような設定内容になります。
const nextConfig: NextConfig = {
output: "standalone",
env: {
API_URL: process.env.API_URL,
NEXT_PUBLIC_TEST_TEXT: process.env.NEXT_PUBLIC_TEST_TEXT,
},
};
上記のように記載した場合に、NEXT_PUBLIC
がついていないAPI_URL
も、
ビルド時に環境変数がインライン化されてしまうことがわかりました。
ドキュメントには、
Next.js will replace process.env.customKey with 'my-value' at build time. Trying to destructure process.env variables won't work due to the nature of webpack DefinePlugin.
(日本語訳) Next.jsはビルド時にprocess.env.customKeyを'my-value'に置き換えます。webpack DefinePluginの性質上、process.env変数を再構築しようとしてもうまくいきません。
と記載されていましたが、
デプロイ後にこのオプションが設定されていることに気づいたため、
調査に時間がかかってしまいました。。
問題の再現
Next.jsは15.3.0
で、下記のファイルを作成します。
サーバーコンポーネント相当のファイル
import MyClientComponent from "./MyClientComponent";
function getApiUrl() {
const url = `${process.env.API_URL}`;
return url;
}
export default function Home() {
getApiUrl();
return (
<div>
<MyClientComponent />
</div>
);
}
クライアントコンポーネント相当のファイル
"use client";
const MyClientComponent = () => {
return (
<div>
<p>{process.env.NEXT_PUBLIC_TEST_TEXT}</p>
</div>
);
};
export default MyClientComponent;
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
環境変数にはNEXT_PUBLIC
の接頭辞あり・なしの2種類用意します。
API_URL=http://localhost:3000
NEXT_PUBLIC_TEST_TEXT=thisistest
FROM node:22 AS base
# ビルド用
FROM base AS builder
WORKDIR /app
# パッケージのインストールと必要なファイルのコピー
COPY package.json package-lock.json* ./
RUN npm ci
# 説明の簡略化のため、.envも含めてコピー
COPY . .
RUN npm run build
# 実行用
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# standaloneモードで生成されたファイルをコピー
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
# publicディレクトリは今回は省略
# COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
この状態でビルドを行います。
docker build -t nextjs-standalone .
ビルドしたDockerイメージを立ち上げて、環境変数の扱いについて確認していきます。
docker run --rm -it nextjs-standalone bash
NEXT_PUBLIC
がついている環境変数の挙動
find
コマンドを使って、NEXT_PUBLIC_TEST_TEXT
を含むファイルを検索します。
find /app -type f -exec grep -l "NEXT_PUBLIC_TEST_TEXT" {} \;
# 実行結果
/app/.env
実行結果は/app/.env
のみになりました。
続いて、環境変数の値を直接検索してみます。
find /app -type f -exec grep -l "thisistest" {} \;
# 実行結果
/app/.next/server/app/index.html
/app/.next/server/app/page.js
/app/.next/static/chunks/app/page-3433830724bb2812.js
/app/.env
今度はいくつかのファイルが該当しました。
試しに、クライアントコンポーネントとしてビルドされた.next/static/chunks/app
配下のファイルを確認してみます。
cat .next/static/chunks/app/page-<hash値>.js | grep "thisistest"
# 実行結果
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[974],{7203:(e,s,i)=>{"use strict";i.d(s,{default:()=>h});var t=i(5155);let h=()=>(0,t.jsx)("div",{children:(0,t.jsx)("p",{children:"thisistest"})})},7520:(e,s,i)=>{Promise.resolve().then(i.bind(i,7203))}},e=>{var s=s=>e(e.s=s);e.O(0,[441,684,358],()=>s(7520)),_N_E=e.O()}]);
上記の実行結果のうち、
"p",{children:"thisistest"}
という部分が環境変数の値部分になります。
これは、下記のコンポーネントの<p>
タグ部分に該当しています。
const MyClientComponent = () => {
return (
<div>
<p>{process.env.NEXT_PUBLIC_TEST_TEXT}</p>
</div>
);
};
つまり、NEXT_PUBLIC
がついた環境変数は、
JavaScriptファイルの中にインラインで埋め込まれることになります。
これは、下記のドキュメントに記載されている挙動です。
NEXT_PUBLIC
がついていない環境変数の挙動
続いて、NEXT_PUBLIC
がついていない環境変数の挙動を確認していきます。
まず、find
コマンドを使って、API_URL
を含むファイルを検索します。
find /app -type f -exec grep -l "API_URL" {} \;
# 実行結果
/app/.next/server/app/page.js
/app/.env
/app/.env
を除いて、/app/.next/server/app/page.js
というファイルが該当しました。
続いて、環境変数の値でも検索してみます。
find /app -type f -exec grep -l "http://localhost:3000" {} \;
# 実行結果
/app/.env
こちらは/app/.env
にのみ該当しました。
つまり、NEXT_PUBLIC
がついていない環境変数は、ビルド時のインライン化は行われていないことになります。
この挙動についても想定通りの挙動です。
NEXT_PUBLIC
がついていない環境変数が、ビルド時に埋め込まれるケース
前述の通り、NEXT_PUBLIC
がついていない環境変数は、ビルド時に埋め込まれないため、JavaScriptファイルにインラインで埋め込まれることはありません。
一方で、NEXT_PUBLIC
がついていないにも関わらず、ビルド時にインライン化されてしまうケースに遭遇しました。
そのケースとは、next.config.ts
ファイルに、env
オプションを設定した場合です。
const nextConfig: NextConfig = {
output: "standalone",
env: {
API_URL: process.env.API_URL,
NEXT_PUBLIC_TEST_TEXT: process.env.NEXT_PUBLIC_TEST_TEXT,
},
};
上記のオプションを設定した状態で、改めて環境変数について確認します。
# 再度ビルドしたDockerコンテナ内で実行
find /app -type f -exec grep -l "API_URL" {} \;
# 実行結果
/app/.next/required-server-files.json
/app/server.js
/app/.env
検索結果に/app/.next/required-server-files.json
というファイルが追加されました。
このファイルは、サーバー起動時に必要な情報をまとめた設定ファイルのようです。
また、環境変数の値でも検索してみると、同様の結果となりました。
find /app -type f -exec grep -l "http://localhost:3000" {} \;
/app/.next/required-server-files.json
/app/server.js
/app/.env
/app/server.js
の中身を確認してみると、下記のような記載になっていました。
const path = require('path')
const dir = path.join(__dirname)
process.env.NODE_ENV = 'production'
process.chdir(__dirname)
const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || '0.0.0.0'
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = {
"env": { "API_URL": "http://localhost:3000", "NEXT_PUBLIC_TEST_TEXT": "thisistest" }
//
// その他の設定
//
}
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
require('next')
const { startServer } = require('next/dist/server/lib/start-server')
if (
Number.isNaN(keepAliveTimeout) ||
!Number.isFinite(keepAliveTimeout) ||
keepAliveTimeout < 0
) {
keepAliveTimeout = undefined
}
startServer({
dir,
isDev: false,
config: nextConfig,
hostname,
port: currentPort,
allowRetry: false,
keepAliveTimeout,
}).catch((err) => {
console.error(err);
process.exit(1);
});
特に、
const nextConfig = {
"env": { "API_URL": "http://localhost:3000", "NEXT_PUBLIC_TEST_TEXT": "thisistest" }
//
// その他の設定
//
}
の部分に注目すると、NEXT_PUBLIC
がついていない環境変数についても
環境変数の値がインラインで埋め込みされてしまっていました。
おわりに
NEXT_PUBLIC
がついていない環境変数でも、
next.config.ts
のenv
オプションに記載した環境変数は
JavaScriptにインラインで埋め込まれてしまうことがわかりました。
今後のNext.jsでの開発時の知見として役立てたいと思います。
Discussion