dotenvは、プロジェクト直下に置いた.envファイルのKEY=VALUEを読み取り、実行中のプロセスの環境変数へ流し込むライブラリです。Node.js・Python・Rubyにそれぞれ同名の実装があり、APIキーやデータベースの接続情報をソースコードから切り離す目的で使われます。2026年9月17日にはnpm版がdotenv 18.0.0へ上がり、node -r dotenv/configによるプリロードが廃止されました。この記事では、.envの書き方と実装ごとの差、複数ファイルを読むときの優先順位、言語別の導入手順、VS Codeで値がターミナルに入らないときの設定、.envをGitへ上げてしまった場合の復旧手順までを扱います。
まとめ:dotenvの要点と判断の目安
dotenvの仕事は「ファイルを読んで環境変数に入れる」の一点だけです。Node.js・Python・Rubyの3実装とNode.js本体の--env-fileはいずれも、すでに設定済みの環境変数を既定では上書きしません。本番のサーバーやCIで環境変数を渡していれば、.envが残っていてもそちらが勝ちます。
- Node.jsで
.envが単純なKEY=VALUEだけなら、Node.js 22.21.0/24.10.0以降の--env-fileで足ります。依存を1つ減らせます。 - npmのdotenvを17系から18系へ上げるときは、
node -r dotenv/configとdotenv_config_path=引数をdotenv runへ書き換えます。18系では引数が黙って無視されます。 - PythonとRubyにはランタイム内蔵の読み込み機能がないため、python-dotenv 1.2.4やdotenv 3.2.0を使います。
- 本番では
.envを置くこと自体を前提にしません。平文のファイルが残るため、シークレット管理サービスか暗号化を挟みます。
dotenvの役割:.envの値をプロセスの環境変数へ流し込む仕組み
設定値をコードに直書きすると、開発用と本番用でソースが分岐し、APIキーがリポジトリに入り込みます。環境変数へ逃がせば解決しますが、開発マシンごとに手作業でexportするのは続きません。dotenvはこの隙間を埋めるライブラリです。読み込んだ値は、Node.jsならprocess.env、Pythonならos.environ、RubyならENVから通常の環境変数と同じように参照できます。環境変数そのものの仕組みやOSごとの設定方法は環境変数とは?Linux・Windowsの設定方法とDocker・CIでの受け渡しで整理しています。
考え方の土台は、設定をコードから分離して環境変数に置くThe Twelve-Factor Appの「設定を環境変数に格納する」です。一方、「既存の環境変数は上書きしない」という既定は各ライブラリの仕様で、Twelve-Factor Appが定めているものではありません。
名前の.envは拡張子ではなく、ファイル名そのものです。先頭がドットなので、macOSやLinuxでは隠しファイルとして扱われます。読み方に公式の定めはなく、「ドットエンブ」「ドットイーエヌブイ」のどちらでも通じます。dotenvは暗号化も権限管理もしません。ファイルを読めるユーザーはすべての値を読めるという性質が、後半で扱う本番運用の分岐点になります。
.envファイルの書き方:共通の記法と実装ごとに割れる仕様
すべての実装で共通する基本記法
# コメント行
DB_HOST=localhost
DB_PORT=5432
API_KEY="sk-xxxxxxxx" # 行末コメント
export NODE_ENV=development
1行に1変数を書きます。値の引用符は省略でき、囲んだ場合の引用符は値に含まれません。行頭のexportは読み飛ばされるため、シェルスクリプトからのコピーがそのまま通ります。もう1つ共通なのが、読み込んだ値はすべて文字列になる点です。DB_PORT=5432は数値の5432ではなく文字列の"5432"として入り、DEBUG=falseも文字列なので、JavaScriptやPythonの条件式では真として評価されます。数値や真偽値として使う箇所では、読み出し側で明示的に変換してください。
複数行・変数展開・コマンド置換の対応差
共通なのは基本記法までです。同じ.envを言語間で使い回すと、次の表の部分で事故が起きます。
| 記法 | Node.js組み込み | dotenv (npm) 18.0.9 | python-dotenv 1.2.4 | dotenv (Ruby) 3.2.0 |
|---|---|---|---|---|
| 引用符付きの複数行 | 対応 | 対応 | 対応 | 対応 |
| ${VAR} の展開 | 非対応 | 非対応 | 対応 | 対応 |
| $VAR の展開 | 非対応 | 非対応 | 非対応 | 対応 |
| $(command) の置換 | 非対応 | 非対応 | 非対応 | 対応 |
| 先頭のBOM | 除去しない | 除去する | 除去する(1.2.3以降) | 除去する |
ROOT_URL=${DOMAIN}/appと書いた.envをNode.js v26.13.0の--env-fileとdotenv 18.0.9で読むと、どちらも${DOMAIN}/appという文字列のまま入ります。npmのdotenvはREADMEのVariable Expansion節で、展開とコマンド置換をdotenvxへ案内しています。一方、python-dotenv 1.2.4とRubyのdotenv 3.2.0ではexample.com/appに展開されました。
注意が要るのはRubyのdotenvで、C="$(echo hi)"と書くと値はhiになります。DATABASE_URL="postgres://$(whoami)@localhost/my_database"のような書き方ができる反面、出所の分からない.envを読み込むとその場でコマンドが実行されます。他人から受け取った.envは、中身を確認してから読み込んでください。
複数の.envを読むときの優先順位:先勝ちと後勝ち
.envと.env.localのように複数のファイルを読むと、同じキーがどちらの値になるかが実装ごとに逆になります。ここを取り違えると、ローカル用の上書きが効かない、あるいは共有設定がローカル値を潰すという症状が出ます。.env.localにSHARED=local、.envにSHARED=dotenvを書き、この順で渡して確かめた結果が次の表です。
| 読み込み方 | 同じキーの扱い | 結果 | 既存の環境変数 |
|---|---|---|---|
| dotenv 18 config({ path: […] }) | 先勝ち | local | 勝つ |
| dotenv 18 同上 + override: true | 後勝ち | dotenv | 負ける |
| dotenv run -f .env.local,.env | 先勝ち | local | 勝つ |
| node –env-file を2回指定 | 後勝ち | 指定順の後 | 勝つ |
| python load_dotenv を2回呼ぶ | 先勝ち | 先に読んだ値 | 勝つ |
| dotenv-rails の自動読み込み | 先勝ち | .local が優先 | 勝つ |
npmのdotenvは「優先したいファイルを先に並べる」、Node.jsの--env-fileは「優先したいファイルを後に並べる」と覚えると混乱しません。Node.jsのCLIドキュメントも、複数指定時は後のファイルが前の値を上書きし、環境とファイルの両方にある変数は環境の値が優先されると明記しています。HELLO=shellをシェルで渡した状態で実行すると、上の表のどの方法でもoverrideを付けない限りshellのままでした。
Pythonで複数ファイルを扱うなら、load_dotenv()を重ねて呼ぶよりdotenv_values()で辞書を作り、{**dotenv_values(".env.shared"), **dotenv_values(".env.secret"), **os.environ}のように右ほど強い形で明示的に合成するほうが、優先順位がコードから読み取れます。
言語・ランタイム別の導入手順とバージョン要件
Node.js:dotenv 18の読み込み方と起動ログ
npm install dotenvで導入し、アプリの最初期に読み込みます。2026年11月1日時点の最新は18.0.9です。CommonJSならrequire('dotenv').config()、ESMならimport 'dotenv/config'を、環境変数を参照するモジュールより先に置きます。
// index.mjs
import 'dotenv/config'
console.log(process.env.DB_HOST)
起動ログ(◇ injected env (4) from .env)の扱いは版ごとに何度も変わっています。17系ではconfig()が標準出力にログを出していましたが、dotenvのCHANGELOGによると18.0.0で出力先が標準エラーへ移り、18.0.4でimport 'dotenv/config'の既定がquiet: trueになりました。dotenv 18.0.9で実測すると、config()のログは2>/dev/nullで消え、import 'dotenv/config'は何も出しませんでした。JSONを標準出力に流すCLIで17系のログが混ざって困っていた場合、18系へ上げるだけで解消します。config()側も消したいときはconfig({ quiet: true })か環境変数DOTENV_QUIET=trueを使います。
Node.js:dotenv 18で廃止されたプリロードとdotenv runへの移行
18.0.0の破壊的変更で影響が大きいのは、プリロードの廃止です。17系まではアプリのコードに手を入れず、node -r dotenv/config index.js dotenv_config_path=.env.localのように起動コマンドで読み込みとファイル指定ができました。18.0.9で同じコマンドを実行すると、.envは読まれるもののdotenv_config_path=は無視され、.env.localの値は入りませんでした。エラーも警告も出ないため、本番で別ファイルを指していた構成ほど気付きにくい変更です。
移行先は18.0.0で追加されたCLIです。
npx dotenv run -- node index.js
npx dotenv run -f .env.local,.env -- node index.js
npx dotenv run --override -q -- npm test
-fは複数指定でき、先に書いたファイルが勝ちます。--overrideで既存の環境変数より.envを優先し、-qでログを抑えます。DOTENV_CONFIG_PATHなどの旧名の環境変数はDOTENV_PATHの代替として引き続き効き、実測でもDOTENV_PATH=.env.local node -r dotenv/configでは.env.localが読まれました。18.0.0では暗号化ファイル.env.vaultのサポートも削除されたため、使っていた場合はdotenvxへの移行が必要です。
紛らわしいのが、別パッケージのdotenv-cli(11.1.0)もdotenvという名前のコマンドを登録する点です。こちらはdotenv -e .env.local -- node index.jsという別の文法で、依存先のdotenvは17系です。両方がインストールされた環境では、どちらのdotenvが呼ばれるかで引数の解釈が変わります。dotenv-cliはdotenv-expandを使って変数展開をするため、${VAR}を使っていないなら本体のCLIへ寄せて外せます。展開が必要で併用するなら、dotenv-cliが同時に登録する別名dotenv-cliで呼び分けます。同じ名前のコマンドはRubyのdotenv gem(dotenv -f ".env.local,.env" ./script.rb)とpython-dotenv(pip install "python-dotenv[cli]"で有効)も登録するため、複数の言語を併用するマシンではnpx dotenvのようにパッケージマネージャー経由で呼ぶと取り違えません。
Node.js組み込み:実験的でなくなった–env-file
Node.jsは--env-fileフラグを本体に持っています。v20.6.0で追加され、v24.10.0とv22.21.0で実験的機能ではなくなりました。2026年11月1日時点のLTSはv26系(Lithium)・v24系(Krypton)・v22系(Jod)で、最新パッチはそれぞれv26.13.0、v24.22.2、v22.23.4です。同じv22系・v24系でも、上記より前のパッチ版では実験的機能の警告が出ます。
node --env-file=.env --env-file=.env.local index.js
node --env-file-if-exists=.env.local index.js
指定したファイルが無いとnode: .nope: not foundで停止します。任意扱いにしたいファイルはv22.9.0で追加された--env-file-if-existsを使うと、.nope not found. Continuing without it.と表示して起動を続けます。コードから読むならprocess.loadEnvFile(path)があり、引数を省略するとカレントディレクトリの.envを読みます。
Python:python-dotenv 1.2.4のload_dotenvとdotenv_values
pip install python-dotenvで導入します。最新の1.2.4(2026年10月1日公開)はPython 3.10以上が必要です。import名はdotenvで、パッケージ名と異なる点に注意してください。
from dotenv import load_dotenv
import os
load_dotenv() # 既存の環境変数は上書きしない
# load_dotenv(override=True) # ファイル側を優先したいときは上の行と置き換える
print(os.getenv("DB_HOST"))
load_dotenv()はパスを省略すると、呼び出したファイルのディレクトリから上位へさかのぼって.envを探します。環境を汚さずに値だけ取り出すならdotenv_values()で、戻り値は辞書です。変数展開は${VAR}形式だけで、$DOMAINのような裸の記述は展開されません。Rubyのdotenv向けに書かれた.envを持ち込むと、この違いで値が壊れます。
Windowsのエディタで保存した.envを読む場合は版も確認してください。python-dotenvのCHANGELOGによると、先頭のBOMを除去する修正は1.2.3(2026年8月16日)で入りました。1.2.2で実測すると、BOM付きファイルの先頭キーは'BOMKEY'という名前で登録され、os.getenv("BOMKEY")はNoneを返しました。
Ruby / Rails:dotenv-rails 3.2.0の読み込み順とテストでの自動復元
Railsではdotenv-railsをGemfileの先頭付近に追加します。本番で.envを読ませないなら、グループを開発とテストに限ります。最新は2025年12月3日公開の3.2.0です。
gem 'dotenv-rails', groups: [:development, :test]
Railsアプリではbefore_configurationのタイミングで自動的に読み込まれ、RAILS_ENVに応じて次の順で適用されます。上のファイルほど優先度が高く、先に設定された値が勝ちます。
| 優先度 | development | test | production | .gitignore |
|---|---|---|---|---|
| 最高 | .env.development.local | .env.test.local | .env.production.local | する |
| 2番目 | .env.local | 読み込まない | .env.local | する |
| 3番目 | .env.development | .env.test | .env.production | しない |
| 最低 | .env | .env | .env | 内容次第 |
テスト環境だけ.env.localが対象外なのは、開発者ごとのローカル設定でテスト結果が変わるのを防ぐためです。3.0以降はテストごとのENV自動復元も入りました。テスト内でENVを書き換えても終了時に元へ戻り、ActiveSupport::TestCaseとRSpecの双方で動きます。無効にするにはconfig.dotenv.autorestore = falseを設定します。dotenvのREADMEによると、climate_controlやice_ageを使っているアプリでは最初から無効です。素のRubyやSinatraではdotenv単体のgemを入れ、起動処理の早い段階でrequire 'dotenv/load'を実行します。VS Codeのデバッガから起動したときに変数が渡らない場合は、ruby on rails を vscode で開発する環境構築のデバッグ設定を確認してください。
dotenvを入れない判断:ランタイム内蔵で足りる条件
Node.jsでは、dotenvを入れない選択が現実的になりました。Node.jsがv22.21.0/v24.10.0以降で、.envが単純なKEY=VALUEの羅列に収まっているなら、--env-fileで十分です。依存が1つ減り、アプリのコードから読み込み処理が消えます。dotenv 18でプリロードが廃止されたため、「コードを触らず起動コマンドだけで読ませたい」という用途では--env-fileのほうが直接の代替になります。
npmのdotenvを選ぶべきなのは、v20.6.0より前のNode.jsでも同じコードを動かす必要がある、BOM付きで保存されたファイルを受け取る可能性がある、config()の戻り値で読み込み結果を扱いたい、のいずれかに当たる場合です。${VAR}で他の変数を参照したいなら、組み込みでもdotenv単体でも展開されないため、dotenvxかdotenv-expandが必要です。Node.js v26.13.0の--env-fileは先頭のBOMを除去しないため、BOM付きファイルの1行目のキーはprocess.envから引けませんでした。config()は戻り値のerrorとparsedで読み込みの成否と内容を返します。
Docker Composeのenv_fileでコンテナに渡す構成なら、アプリ側の読み込みは不要です(構成例はdocker-composeとは?複数コンテナをymlで定義し一括管理する仕組み)。NestJSなら@nestjs/config(ConfigModule)の使い方のほうが検証まで含めて扱えます。dotenvと二重に読み込むと値の出所が追えなくなるので、読み込む主体は1つに絞ります。
VS Codeで「ターミナル環境への取り込みは無効」と表示されたときの設定
VS CodeでPythonのプロジェクトを開くと、「環境ファイルは構成されていますが、ターミナル環境への取り込みは無効になっています」という通知が出ることがあります。.envの場所は設定済みなのに、統合ターミナルへ値を注入する設定がオフになっている、という意味です。設定項目はpython.terminal.useEnvFileで、Python Environments拡張(vscode-python-envs)が提供しています。VS CodeのPython設定リファレンスでも既定値はfalseで、初期状態ではこの通知が出るのが正常です。
- 設定画面(Ctrl+,/Cmd+,)で
python.terminal.useEnvFileを検索し、チェックを入れる。settings.jsonなら"python.terminal.useEnvFile": trueを追加する。 .envがワークスペース直下にない場合は、python.envFileにファイルのパスを指定する。- 既存のターミナルを閉じて開き直す。注入はターミナルを作るときにしか行われない。
この設定は統合ターミナルに値を入れるためのもので、Pythonのコードからos.getenv()で読むだけならpython-dotenvのload_dotenv()で足ります。ターミナルでpython manage.pyのようなコマンドを直接打ち、.envの値を前提にしたい場合に有効にします。
.envに書くもの・書かないもの:本番でのシークレット管理
.envに入れるのは、データベースの接続先や外部APIのキーのように、環境ごとに変わる値と直書きしたくない資格情報です。全環境で同じ定数は、通常の設定ファイルに置いたほうが変更履歴を追えます。
本番でどこまで.envに頼るかは、平文とローテーションの2点で決まります。攻撃者にファイルの読み取り権限を取られれば.envの全件が読まれ、配布を自動化していなければ鍵の入れ替えを複数台へ手作業で配ることになります。この2点を許容できない本番系では、AWS Secrets Managerとは?料金・ローテーション設定と採用判断で整理しているようなシークレット管理サービスへ寄せます。開発機で.envの平文そのものを無くしたいなら、1Password CLIのop runで.envの平文をなくす手順のように、起動時に参照文字列を実値へ差し替える方式もあります。Railsなら標準のconfig/credentials.yml.encとマスターキーの組み合わせが選べます。
.envの形式を保ったまま値を暗号化してリポジトリに入れたい場合は、dotenvxの仕組みと移行手順が候補です。設定とシークレットの分離を含めてアプリの設計から見直す場合は、フルスクラッチ開発の段階で配布経路まで決めておくと、後から.envを抜く改修が要りません。
.envをGitに載せない運用と、上げてしまった場合の復旧手順
基本の防御は、.gitignoreに.envを書くことと、キーの一覧だけを持つ.env.exampleをコミットしておくことです。Railsの表のとおり.env.developmentのような共有設定を追跡対象に残す運用もありますが、そのファイルに機密を書かない規律とセットでなければ意味がありません。
機密を含む.envをpushしてしまった場合は、手を付ける順番で結果が変わります。GitHubの「リポジトリからの機微なデータの削除」は、履歴を書き換えるより先に、漏れた鍵やトークンを失効・再発行することを求めています。公開された時点で、その値は漏洩済みと考えるべきだからです。
- 漏れた鍵をただちに失効・再発行する。これだけで実害を止められる場合が多い。
- 履歴からの削除が必要なら、GitHubが案内している
git filter-repoで書き換えてforce pushする。--sensitive-data-removalフラグを持つ2.47以上が必要。 - 書き換え前のクローンを持つメンバーが
git pullとgit pushをすると機密が戻るため、チームへ再クローンを周知する。 - フォークには残り続け、プルリクエストのキャッシュ表示はGitHubサポートへの依頼でしか消せない。だから1の失効を省略しない。
予防では、GitHubのsecret scanningとpush protectionが使えます。公開リポジトリではsecret scanningが無料で自動実行され、公開リポジトリへのpushを止めるユーザー単位のpush protectionも既定で有効です。ただし検出範囲は機能・プラン・設定で変わり(サポートされるパターンの一覧)、自社独自形式のキーまで確実に拾える保証はありません。.gitignoreとレビューが一次防御であることは変わりません。CIでの受け渡しはGitHub ActionsのSecrets管理で扱っています。
.envの値が読み込まれないときの確認順
- 読み込みの位置:設定値を参照するモジュールより後でdotenvを読み込むと、参照した時点では未設定です。ESMでは
import 'dotenv/config'を最初のimportにします。 - カレントディレクトリ:npmのdotenvの既定パスは
process.cwd()直下の.envです。サブディレクトリから起動すると見つかりません。config({ path: ... })で明示します。 - 既存の環境変数:シェルやCIで同じ名前が設定済みなら、
.envの値は既定では使われません。古い値が残っていないかenv | grep 変数名で確かめます。 - BOM:1行目の変数だけが読めない場合、Node.jsの
--env-fileや1.2.2以前のpython-dotenvを使っているなら、ファイルがBOM付きUTF-8で保存されていないかを確認します。BOM付きなら、エディタで「UTF-8(BOMなし)」で保存し直します。 - dotenv 18への更新直後:
dotenv_config_path=引数で別ファイルを指していたなら、前述のプリロード廃止が原因です。
npmのdotenvではconfig()の戻り値のerrorとparsedを見れば、ファイルを開けなかったのか、開けたが目的のキーが無いのかを切り分けられます。
よくある質問
dotenvとdotenv-railsのgemはどう違いますか?
dotenvが本体で、dotenv-railsはRails向けの薄いラッパーです。dotenv-railsを入れるとRailsの起動時にRAILS_ENVに応じた.env.developmentや.env.testを自動で読み込み、テストでのENV自動復元も有効になります。Rails以外では本体のdotenvを入れ、require 'dotenv/load'で読み込みます。バージョンはどちらも3.2.0です。
.envファイルはどこに置けばよいですか?
既定の探索場所は実装によって異なります。npmのdotenvとNode.jsの--env-fileはカレントディレクトリを基準に解決するので、アプリを起動するディレクトリの直下に置きます。python-dotenvのload_dotenv()は呼び出したファイルのディレクトリから上位へさかのぼって探します。起動場所が変わるスクリプトでは、パスを明示して渡すほうが確実です。
dotenvはpnpmやyarnでも同じように使えますか?
使えます。dotenvは実行時にファイルを読むライブラリなので、pnpm add dotenvやyarn add dotenvで入れれば読み込み方は同じです。CLIもpnpm exec dotenv run -- node index.jsのように呼べます。パッケージマネージャー自身の設定ファイル(.npmrc)は.envとは別系統なので、認証トークンを書く場合は別途.gitignoreの対象にしてください。
dotenvの最新バージョンはいくつですか?
2026年11月1日時点で、npmのdotenvは18.0.9(npmレジストリへの公開は2026年10月22日)、python-dotenvは1.2.4(同10月1日)、Rubyのdotenvとdotenv-railsは3.2.0(2025年12月3日)です。npm版は18.0.0から18.0.9まで約1か月で9回のパッチが出ているため、固定する版はnpmのパッケージページで確認してください。
DOTENV_CONFIG_QUIETはdotenv 18でも使えますか?
使えます。18系の正式名はDOTENV_QUIETですが、旧名のDOTENV_CONFIG_QUIETも代替として読まれます。両方ある場合はDOTENV_QUIETが優先されます。なお18.0.4以降はimport 'dotenv/config'が既定で無出力なので、ESMで使っているなら設定自体が不要になっている場合があります。