できない.dev

Node.js で dotenv の値が読み込めない

.env が読まれない原因はほぼ「呼ぶ順序が遅い」「ファイルの場所が違う」「Node 20.6+ の --env-file と二重読み」のいずれか。
エントリで最初に dotenv.config() を呼び、import 順を意識すれば解決する。

公開: 更新:

実行例あり(2026-08-02 に実環境で検証)

要約

dotenv が読まれないと感じる場面のほとんどは「読み込みのタイミング」が遅い。
他モジュールの import 評価より 先に dotenv.config() を実行する必要があり、ESM では特に順序が問題になる。
Node.js 20.6 以降は標準で --env-file(新しいタブで開く) を持っているため、新規プロジェクトでは dotenv 自体不要なケースも増えている。

よくある原因

  1. 呼び順が遅い: import config from "./config" の中で process.env.FOO を読むのに、dotenv.config() がその後ろにある
  2. カレントディレクトリのズレ: npm start を別ディレクトリから走らせ、.env がプロジェクトルートに無いと dotenv は見つけられない
  3. ESM の評価順: ESM の import はトップレベルで実行されるが、同じファイル内の通常文より前に動く。dotenv.config()import の後ろに書いても、他 import には間に合わない
  4. --env-file との二重読み: node --env-file=.env app.jsdotenv.config() の両方を併用すると、片方が空文字で上書きすることがある

解決策

1. エントリ最上部で読み込む

CommonJS の場合。

// index.js
require("dotenv").config();
const app = require("./app");   // この時点で process.env が揃っている

ESM の場合。

// main.mjs
import "dotenv/config";          // ← 最初の import
import { app } from "./app.mjs";

dotenv/config を import するだけで config() が自動実行される。

2. パスを絶対指定する

import { config } from "dotenv";
import path from "node:path";
import { fileURLToPath } from "node:url";
 
const __dirname = path.dirname(fileURLToPath(import.meta.url));
config({ path: path.resolve(__dirname, "../.env") });

npm start の起動位置に依存しなくなる。

3. --import で preload する(ESM)

node --import "dotenv/config" app.mjs

--import フラグで指定したモジュールは、アプリの最初の import より前に評価される。package.jsonscripts に書いておく。

{
  "scripts": {
    "start": "node --import dotenv/config app.mjs"
  }
}

4. Node 20.6+ なら --env-file 単独に

node --env-file=.env app.js

dotenv ライブラリを外し、Node 標準機能だけで完結させる。
複数の env ファイルを読みたい場合は --env-file を複数回指定できる(後勝ち)。

実行例

実際に上記の手順を node:20 コンテナ(dotenv 17.4.2)で動かすと、dotenv.config() より前に process.env.KEY を参照したケースでは期待どおり undefined が返り、エントリ最上部で読み込んだケースでは secret-123 が取得できる。
ログ中の ◇ injected env (1) from .env // tip: … という行は dotenv v17 以降が標準で出す読み込みメッセージで、// tip: 以降は実行のたびにランダムに変わる(姉妹ツール dotenvx の宣伝が出ることもあるが、dotenvx を呼んでいるわけではない)。
このログを消したい場合は config({ quiet: true }) を渡せばよい。
NG ケースではこのログが 読めた値: undefined に出ている点にも注目してほしい。config() の実行そのものが参照より遅いことがログの順序からも分かる。

$ node bad.js
◇ injected env (1) from .env // tip: ◈ encrypted .env [www.dotenvx.com]
読めた値: undefined
$ node good.js
◇ injected env (1) from .env // tip: ⌘ override existing { override: true }
読めた値: secret-123
終了コード: 0
$ node --version
v20.20.2
$ node --env-file=.env -e "console.log('--env-file:', process.env.API_KEY)"
--env-file: secret-123
終了コード: 0

— 2026-08-02 時点の出力

検証環境

検証日
実行環境
node:20

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

この記事は役立ちましたか?