はじめに
APIキーやデータベースのパスワードを、うっかりコードの中に直書きしてそのままgitにコミットしてしまった経験はないでしょうか。一度履歴に残ってしまった秘密情報は、あとから削除しても完全には消えません。プッシュした瞬間に、GitHub上の誰かに見られてしまうリスクがあります。
こうした事故を防ぐ定番の方法が、設定値をコードから切り離して.envファイルに書き、実行時に読み込むというやり方です。Node.jsのエコシステムでこの役割を長年担ってきたのがDotenvです。ゼロ依存の小さなモジュールでありながら、Twelve-Factor Appの原則に沿った「設定とコードの分離」を、たった数行で実現できます。
Dotenvとは
Dotenvは、.envファイルに書かれたKEY=VALUE形式の設定を読み込み、process.envに登録するNode.js用のライブラリです。開発環境やCIごとに異なる値を持ちたいAPIキー、DB接続文字列、ポート番号などを、コードを一切変更せずに切り替えられるようにします。
主な特徴
- ゼロ依存モジュール - 追加のパッケージを引き連れず、インストールも読み込みも軽量
- Twelve-Factor App準拠 - 設定をコードから分離するというベストプラクティスに沿った設計
- シンプルなAPI -
config()を呼ぶだけで動く手軽さと、parse()populate()による細かい制御の両方に対応 - エコシステムの土台 -
dotenv-expand(変数展開)や、暗号化に対応した後継のdotenvxなど、周辺ツールの共通基盤になっている
インストール
npm、yarn、pnpmのいずれでも導入できます。
npm install dotenv --save
yarn add dotenv
pnpm add dotenv
基本的な使い方
プロジェクトのルートに.envファイルを置き、KEY=VALUEの形式で設定を書きます。
# .env
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
API_KEY=sk_test_xxxxxxxxxxxx
アプリケーションの起動時にできるだけ早いタイミングでdotenvを読み込むと、.envの中身がprocess.envに展開されます。
// index.js
require('dotenv').config()
// もしくは ESM の場合
// import 'dotenv/config'
console.log(process.env.DB_HOST) // "localhost"
console.log(process.env.API_KEY) // "sk_test_xxxxxxxxxxxx"
config()は{ parsed, error }形式のオブジェクトを返すため、読み込みに失敗した場合の分岐も書けます。
const result = require('dotenv').config()
if (result.error) {
throw result.error
}
console.log(result.parsed) // 読み込んだキーと値のオブジェクト
Dotenvはブラウザ上で完結するライブラリではなく、Node.jsのfsモジュールを使ってファイルシステムから.envを読み込む仕組みのため、この記事では実行可能なプレイグラウンドは用意していません。手元のNode.js環境でぜひ試してみてください。
実践的なユースケース
環境ごとに読み込む.envファイルを切り替える
開発・テスト・本番で異なる設定を持ちたいとき、.env.developmentや.env.productionのようにファイルを分け、DOTENV_CONFIG_PATHで読み込み先を指定する方法がよく使われます。
// NODE_ENVに応じて読み込むファイルを切り替える
const path = `.env.${process.env.NODE_ENV || 'development'}`
require('dotenv').config({ path })
console.log(`Loaded config from ${path}`)
コマンドラインから直接パスを指定することもできます。CIやDockerコンテナのように環境変数で制御したい場面に向いています。
DOTENV_CONFIG_PATH=/config/.env.production node -r dotenv/config index.js
parseとpopulateで読み込みを個別に制御する
config()はファイルの読み込みからprocess.envへの反映まで一括で行いますが、parse()とpopulate()を使うと、ファイル読み込み以外の入力(テスト用の文字列や別プロセスの出力など)もパースでき、反映先も自分で選べます。
const fs = require('fs')
const dotenv = require('dotenv')
// 文字列やBufferを直接パースする
const buf = Buffer.from('PORT=3000\nHOST=example.com')
const config = dotenv.parse(buf)
console.log(config) // { PORT: '3000', HOST: 'example.com' }
// 任意のオブジェクトに反映する(process.envを汚さない)
const target = {}
dotenv.populate(target, config, { override: true })
console.log(target.PORT) // '3000'
テストコードで一時的な設定値だけを注入したい場合や、process.envを直接触りたくないライブラリコードの中で使うと役立ちます。
複数行の値とコメントを扱う
秘密鍵(RSA/PEMなど)のように改行を含む値は、ダブルクォートで囲むことでそのまま1つの環境変数として扱えます。行頭の#はコメントとして無視されます。
# .env
# データベースの秘密鍵(複数行対応)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
# コメント行は読み込み時に無視される
DEBUG=false
require('dotenv').config()
console.log(process.env.PRIVATE_KEY.includes('BEGIN RSA PRIVATE KEY')) // true
読み込み時のログを抑制したいCI環境などでは、DOTENV_CONFIG_QUIET=trueやconfig({ quiet: true })を指定すると、コンソールへの出力を止められます。
まとめ
Dotenvは「.envファイルをprocess.envに読み込む」という一点に絞られた、小さくて枯れたライブラリです。config()だけで始められる手軽さがありながら、parse()やpopulate()で細かい制御にも対応でき、環境ごとの設定切り替えという地味だけれど欠かせない作業を安定して支えてくれます。
もし.envファイル自体を暗号化してgitに含めたい、チームで安全に共有したいといった一歩進んだニーズが出てきたら、Dotenvと互換性を保ちながら暗号化に対応した後継ツールdotenvxも選択肢に入ってきます。まずは今回の内容で、秘密情報をコードから切り離すところから始めてみてください。