⚡️

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

に公開3
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