⚡️

Next.jsにおけるenvのベストプラクティス

2021/11/03に公開3

Next.js で env をうまく扱うために僕がよく使う手法を紹介します。

Next.js がサポートしている env の扱い

Next.js はデフォルトで大きく 2 つの方法で env をサポートしています。

  1. .env ファイルの読み込み
  2. next.config.js の env キーに記述する

.env ファイルの読み込み

Next.js は .env ファイルを配置することで process.env に読み込む機能をデフォルトでサポートしています。なのでプロジェクトのルートに、以下のようなファイルを配置してください。

.env
API_ORIGIN=http://localhost:8080

Next.js のプロジェクトでは process.env.DB_HOST で読み込むことができます。

ref: https://nextjs.org/docs/basic-features/environment-variables

next.config.js の env キーの記述する

Next.js の設定ファイルである next.config.js には env というキーが存在します。

next.config.js
module.exports = {
  env: {
    customKey: 'my-value',
  },
}

ref: https://nextjs.org/docs/api-reference/next.config.js/environment-variables

NODE_ENV の存在

環境変数を扱うために、よくあるのは NODE_ENVdevelopmentproduction などの環境を指す文字列を挿入し、 process.env.NODE_ENV により実装を分岐させるということをよくやります。

いろんなソースコードでこの使い方はされており、 Next.js もまた NODE_ENV を扱っている実装があります。

next.ts
const defaultEnv = command === 'dev' ? 'development' : 'production'

const standardEnv = ['production', 'development', 'test']

if (process.env.NODE_ENV && !standardEnv.includes(process.env.NODE_ENV)) {
  log.warn(NON_STANDARD_NODE_ENV)
}

;(process.env as any).NODE_ENV = process.env.NODE_ENV || defaultEnv

ref: https://github.com/vercel/next.js/blob/7e370134fb46dede3809df664db95c603a5a2997/packages/next/bin/next.ts#L87-L95

上記の実装でもあるように、 Next.js は以下の NODE_ENV しかサポートしていません。

  • production
  • development
  • test

ただし、現場で Next.js を使っていると「staging 環境では staging.api.example.com が API のオリジンになる」とかがあり、Next.js がサポートしていない NODE_ENV を使いたい需要はあります。

こう言った場合に使える方法として APP_ENV などのように別の環境変数を作成する方法です。

APP_ENV を使った Next.js プロジェクトの構成

APP_ENV を使うと NODE_ENV ではサポート出来なかった staging などの設定をうまく書き分けることができます。そこで使うのは next.config.js の env キーによる環境変数の読み込みです。

next.config.js を以下のように設定します。

next.config.js
module.exports = {
  env: {
    ...require(`./config/${process.env.APP_ENV || 'local'}.json`),
  },
}

プロジェクトのルートに config というディレクトリを作成し、以下のようにファイルを作成します。

config/local.json
{
  "API_ORIGIN": "http://localhost:8080"
}

このプロジェクトを yarn dev などで起動すると next.config.js を読み込んだ際に require をするためその先のファイルを読みに行きます。 NODE_ENVdevelopment になり、 APP_ENV は設定してないので local になるので config/local.json を読み込みます。 JSON ファイルを読み込んだらスプレッド演算子により最終的に next.config.js の env は以下のようになります。

module.exports = {
  env: {
    API_ORIGIN: "http://localhost:8080",
  },
}

この方法であればどんな環境に対してでも対応ができます。

config/staging.json を作成して以下のようなファイルを作成します。

config/staging.json
{
  "API_ORIGIN": "https://staging.api.example.com"
}

APP_ENV=staging yarn dev として起動する、または Docker などの環境に対して APP_ENV=staging と設定してから yarn start を行えば、 API_ORIGINhttps://staging.api.example.com になっています。

GitHubで編集を提案

Discussion

akuaku

この方法シンプルで素敵で採用させていただいています!

.env で定義する方法だと .env.development, .env.production くらいしか分けることができないですが、この方法だと APP_ENV で無制限に環境を切り替えられますし、JSONで定義するからTypeScript型定義の生成も簡単にできますね。

しかし、 .env の方法に物足りなさを感じた読者がこの方法に乗り換えて内部挙動を理解せずに使った時に、上手く動作しなかったりセキュリティ上の問題が発生してしまう恐れがあると感じました。

実際に自分がハマったのですが他の方にも参考になればと思いコメントさせていただきます。

1. 外部ライブラリから参照できない

NextAuth を使っているときに process.env.NEXTAUTH_URL を NextAuth が認識してくれなくて気づいたのですが、 next.config.js の env に入れた値は Webpack の DefinePlugin を使ってビルド時に置換されるので、Webpack を通さない外部ライブラリからは参照できません。
外部ライブラリ(ここでは next-auth)はランタイムで process.env.NEXTAUTH_URL を解決しようとするので、 process.env オブジェクトにも値を入れておく必要がありました。

const env = require(`./config/${process.env.APP_ENV || 'local'}.json`);

Object.entries(env).forEach(([key, value]) => {
  process.env[key] = value;
});

module.exports = {
  env,
}

2. ブラウザー側にシークレットな値が流出してしまう可能性がある

DefinePlugin で置換されているということは、もしブラウザー側にバンドルされるコードにシークレットな環境変数を参照するコードが混ざっていたら置換されてしまい、流出してしまいます。

例えば JSON に "SECRET_TOKEN": "my_secret_token" が定義されているとして、Reactコンポーネント内で process.env.SECRET_TOKEN を参照するとブラウザーにバンドルされる js に my_secret_token が混入してしまいます。

.env のやり方だと NEXT_PUBLIC_ のプレフィックスが付いている項目のみブラウザー側から参照できるようになっているのでその辺り安心なのですが、それに安心しているユーザーがこちらに移行した場合、知らずのうちにシークレットな値を公開してしまう可能性があります。

.env に倣って NEXT_PUBLIC_ が付いたもののみブラウザー側に公開しようとするとこうなりました。

const env = require(`./config/${process.env.APP_ENV || 'local'}.json`);

const publicEnv = {}
Object.entries(env).forEach(([key, value]) => {
  if(key.startsWith("NEXT_PUBLIC_")) {
    publicEnv[key] = value;
  }

  process.env[key] = value;
});

module.exports = {
  env: publicEnv,
}

完全版

ここまで調整した後に「.env ではどうやって NEXT_PUBLIC_ だけ公開しているんだろう」と思って Next.js のソースをみると、 DefinePlugin に渡す時に process.env から NEXT_PUBLIC が付いているものだけフィルターしていました。
https://github.com/vercel/next.js/blob/384953b35c5e9935bb4a2fcdfe5056efb73cd740/packages/next/build/webpack-config.ts#L1298-L1307

一方で env に渡したものはそのまま DefinePlugin に渡されていました。
https://github.com/vercel/next.js/blob/384953b35c5e9935bb4a2fcdfe5056efb73cd740/packages/next/build/webpack-config.ts#L1307-L1313

ということは env に入れなくても process.env にだけ入れておけば、Next.js がいい感じに NEXT_PUBLIC_ が付いたものだけブラウザー側でも参照できるようにしてくれるので、最終的に next.config.js の中身はこれで良いことになりました。

const env = require(`./config/${process.env.APP_ENV || 'local'}.json`);

Object.entries(env).forEach(([key, value]) => {
  process.env[key] = value;
});

module.exports = {}

読者の方にも参考になればと思います!

JJJJ

ありがとうございます!!

これ、Next.js 12.3 ではいる .env の編集に対する file-watch してるのでこの方法だと next dev などを一旦やり直さないといけない(HMRできない)というデメリットが生まれましたね🤔

akuaku

これ、Next.js 12.3 ではいる .env の編集に対する file-watch してるのでこの方法だと next dev などを一旦やり直さないといけない(HMRできない)というデメリットが生まれましたね🤔

そうですね 😔 今からNext.jsでenvの仕組みを構築する場合は、

  • ローカル開発時はNext.jsのenvの仕組みを使ってホットリロードの恩恵を受ける
  • ビルド・デプロイ時はDockerを使った公式のExampleにあるように、ビルド前に .env.{APP_ENV}.env.production にコピーする

のが良いかなと思います。

https://github.com/vercel/next.js/blob/6bfd1458b2913239779f810d93ee13bb14e98076/examples/with-docker-multi-env/docker/staging/Dockerfile#L25-L27