Zenn
📝

NEXT_PUBLICがついていない環境変数もインライン化されてしまった話

に公開
1

はじめに

Next.jsのコンテナをECSにデプロイする際に、
タスク定義の環境変数が参照されない問題に遭遇しました。

実際に調べてみると、NEXT_PUBLICがついていない環境変数も、
ビルド時の値でインライン化されてしまっていたことがわかりました。

上記の現象が発生した原因について調べた内容について記載しようと思います。

結論

next.config.tsenvオプションを設定していることが原因でした。
具体的には、下記のような設定内容になります。

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変数を再構築しようとしてもうまくいきません。

と記載されていましたが、
デプロイ後にこのオプションが設定されていることに気づいたため、
調査に時間がかかってしまいました。。

https://nextjs.org/docs/app/api-reference/config/next-config-js/env

問題の再現

Next.jsは15.3.0で、下記のファイルを作成します。

サーバーコンポーネント相当のファイル

page.tsx
import MyClientComponent from "./MyClientComponent";

function getApiUrl() {
  const url = `${process.env.API_URL}`;

  return url;
}

export default function Home() {
  getApiUrl();

  return (
    <div>
      <MyClientComponent />
    </div>
  );
}

クライアントコンポーネント相当のファイル

MyClientComponent.tsx
"use client";

const MyClientComponent = () => {
  return (
    <div>
      <p>{process.env.NEXT_PUBLIC_TEST_TEXT}</p>
    </div>
  );
};

export default MyClientComponent;
next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

環境変数にはNEXT_PUBLICの接頭辞あり・なしの2種類用意します。

.env
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 --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /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ファイルの中にインラインで埋め込まれることになります。

これは、下記のドキュメントに記載されている挙動です。
https://nextjs.org/docs/app/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser

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.tsenvオプションに記載した環境変数は
JavaScriptにインラインで埋め込まれてしまうことがわかりました。

今後のNext.jsでの開発時の知見として役立てたいと思います。

1
Aidemy Tech Blog

Discussion

ログインするとコメントできます