.envの#以降が消える Node・dotenv・Composeの差を実測

.env に DB_PASSWORD=abc#123 と書いたら、アプリには abc しか渡っていなかった。原因は # 以降がコメントとして捨てられたことだが、同じファイルを Docker Compose に読ませると abc#123 のまま通る。どちらも「.env を読む」道具なのに、# の扱いが違う。

この記事では、同じ .env を Node.js 組み込みのパーサー、npm の dotenv、python-dotenv、Docker Compose の 4 つで読み、値がどう変わるかを実測する。

同じ .env を4つのパーサーで読む

次の sample.env を用意した。パスワードや URL、色コードなど、# が入りがちな値を並べている。

1
2
3
4
5
6
7
8
9
10
11
PASS1=abc#123
PASS2=abc #123
PASS3="abc#123"
PASS4='abc #123'
URL=https://example.com/#top
COLOR=#ff0000
EMPTY=
SPACED= hello world
MULTI="line1\nline2"
SINGLE='line1\nline2'
export EXPORTED=yes

実行環境は Windows 11、Node.js v24.11.1(util.parseEnv と --env-file)、dotenv 18.0.9、Python 3.11.9 + python-dotenv 1.2.4、Docker Compose v5.5.1(docker compose config の出力)。結果を並べる。

キー Node.js v24.11.1 dotenv 18.0.9 python-dotenv 1.2.4 Docker Compose v5.5.1
PASS1 "abc" "abc" "abc#123" "abc#123"
PASS2 "abc" "abc" "abc" "abc"
PASS3 "abc#123" "abc#123" "abc#123" "abc#123"
PASS4 "abc #123" "abc #123" "abc #123" "abc #123"
URL "https://example.com/" "https://example.com/" "https://example.com/#top" "https://example.com/#top"
COLOR "" "" "#ff0000" "#ff0000"
SPACED "hello world" "hello world" "hello world" "hello world"
MULTI 改行入り 改行入り 改行入り 改行入り
SINGLE \n が文字のまま \n が文字のまま \n が文字のまま \n が文字のまま
EXPORTED "yes" "yes" "yes" "yes"

食い違うのは PASS1、URL、COLOR の 3 行で、どれも「引用符なしの値の途中に、空白を挟まずに # がある」行だ。Node.js と dotenv は # の位置で値を切り、python-dotenv と Compose は切らない。COLOR=#ff0000 は Node.js と dotenv では空文字列になる。

引用符で囲んだ PASS3 / PASS4 と、空白の後に # がある PASS2 は 4 つとも同じ結果だった。二重引用符の中では \n が改行に、一重引用符の中では文字のまま残る点も揃っている。

Node.js は node --env-file=sample.env と process.loadEnvFile('sample.env') でも試し、util.parseEnv と同じく PASS1 が "abc"、URL が "https://example.com/"、COLOR が "" になった。

なぜ食い違うのか: 「# の前に空白が要るか」

各実装のドキュメントとコードを見ると、コメントの始まりの決め方が 2 系統に分かれている。

Node.js と dotenv: # があればそこからコメント

Node.js のドキュメント(Environment Variables の DotEnv 節)は、# がコメントの始まりで行の残りは無視され、引用符の中の # は普通の文字として扱う、と説明している。--env-file の説明でも「# の後のテキストはコメント」とある。

npm の dotenv の README も、# があるところからコメントが始まるので、値に # を含めるなら引用符で囲むよう書いている。これは v15.0.0 からの破壊的変更とされている。インストールした 18.0.9 のパーサーでも、引用符なしの値は [^#\r\n]+ という正規表現で取り出しており、# の手前で止まる。

python-dotenv と Docker Compose: 空白の後の # だけがコメント

Docker Compose のドキュメント(Interpolation の .env 構文の節)は、引用符なしの値の行内コメントは空白の後に置く必要があると明記し、VAR=VAL# not a comment は VAL# not a comment になる例を載せている。

python-dotenv の README は「値の後にコメントを書ける」とだけ書いていて # の規則は明記していない。パーサー(dotenv/parser.py)を読むと、引用符なしの値に re.sub(r"\s+#.*", "", part) を当てており、空白に続く # 以降だけを落としている。

境界のケース: 空白とタブ

「空白の後」の解釈にも差がある。次の 3 行で試した。

1
2
3
NOVAL=#novalue
SPCOMMENT= # comment
TAB=abc #123

TAB の abc と #123 の間はタブ 1 文字である。

キー Node.js / dotenv python-dotenv 1.2.4 Docker Compose v5.5.1
NOVAL "" "#novalue" "#novalue"
SPCOMMENT "" "" "# comment"
TAB "abc" "abc" "abc\t#123"

SPCOMMENT= # comment は python-dotenv では空、Compose では # comment という値になった。python-dotenv のパーサーには「= の後に空白があって値が # で始まるなら空の値とコメント」という分岐があり、それが効いている。Compose では = 直後の空白が落ち、# から始まる文字列が値として残った。タブの後の # は Compose ではコメントにならなかった。ドキュメントの「space」の表記どおりの挙動である。

つまり、空白の後に # を書いても 4 つで揃うとは限らない。コメントは独立した行に書くのがいちばん確実だ。

実際に試す

1. 4つのパーサーで読み比べる

Node.js(記事では v24.11.1 で確認。util.parseEnv が無い古いバージョンでは動かない)と Python 3、Docker が入った環境で試せる。作業用ディレクトリに上の sample.env を置き、dotenv と python-dotenv を入れる。

1
2
3
npm init -y
npm i dotenv@18.0.9
python -m pip install --target pydeps python-dotenv

Node.js 側の比較スクリプト compare.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('node:fs');
const util = require('node:util');
const dotenv = require('dotenv');

const src = fs.readFileSync(process.argv[2] || 'sample.env', 'utf8');

const a = util.parseEnv(src);
const b = dotenv.parse(src);

const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])];
console.log('node', process.version, 'dotenv', require('dotenv/package.json').version);
for (const k of keys) {
console.log(k.padEnd(9), JSON.stringify(a[k]).padEnd(22), JSON.stringify(b[k]));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
$ node compare.js
node v24.11.1 dotenv 18.0.9
COLOR "" ""
EMPTY "" ""
EXPORTED "yes" "yes"
MULTI "line1\nline2" "line1\nline2"
PASS1 "abc" "abc"
PASS2 "abc" "abc"
PASS3 "abc#123" "abc#123"
PASS4 "abc #123" "abc #123"
SINGLE "line1\\nline2" "line1\\nline2"
SPACED "hello world" "hello world"
URL "https://example.com/" "https://example.com/"

Python 側の compare.py。

1
2
3
4
5
6
7
import json, sys
from importlib.metadata import version
from dotenv import dotenv_values

print("python-dotenv", version("python-dotenv"))
for k, v in dotenv_values(sys.argv[1] if len(sys.argv) > 1 else "sample.env").items():
print(k.ljust(9), json.dumps(v))
1
2
3
4
5
6
7
8
9
10
11
12
13
$ PYTHONPATH=pydeps python compare.py
python-dotenv 1.2.4
PASS1 "abc#123"
PASS2 "abc"
PASS3 "abc#123"
PASS4 "abc #123"
URL "https://example.com/#top"
COLOR "#ff0000"
EMPTY ""
SPACED "hello world"
MULTI "line1\nline2"
SINGLE "line1\\nline2"
EXPORTED "yes"

Docker Compose は、env_file で読ませた値を docker compose config で確認できる。コンテナを起動しなくてよい。

1
2
3
4
5
# compose.yaml
services:
app:
image: alpine
env_file: sample.env
1
2
3
4
5
6
$ docker compose config --format json
...
"PASS1": "abc#123",
"PASS2": "abc",
...
"URL": "https://example.com/#top"

2. 引用符なしの # を含む行を検出する

どのパーサーでも同じ値にするには、# を含む値を引用符で囲めばよい。囲み忘れを探す小さなスクリプトを置いておく。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// check-hash.js: 引用符なしの値に # を含む行を探す
const fs = require('node:fs');
const file = process.argv[2] || '.env';
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
let found = 0;
lines.forEach((raw, i) => {
const line = raw.replace(/^\s*(export\s+)?/, '');
if (line === '' || line.startsWith('#')) return;
const eq = line.indexOf('=');
if (eq <= 0) return;
const value = line.slice(eq + 1).trimStart();
if (value.startsWith('"') || value.startsWith("'") || value.startsWith('`')) return;
if (value.includes('#')) {
found++;
console.log(`${file}:${i + 1}: ${line.slice(0, eq).trim()} の値に引用符なしの # があります`);
}
});
process.exitCode = found ? 1 : 0;
1
2
3
4
5
$ node check-hash.js sample.env
sample.env:1: PASS1 の値に引用符なしの # があります
sample.env:2: PASS2 の値に引用符なしの # があります
sample.env:5: URL の値に引用符なしの # があります
sample.env:6: COLOR の値に引用符なしの # があります

見つかると終了コード 1 を返すので、CI や pre-commit に入れられる。PASS2=abc #123 のように意図した行内コメントも拾うが、前節のとおり行内コメント自体がパーサー間で揃わないので、独立した行に移すきっかけとして扱えばよい。

ハシトシステムの .env⇄JSON 変換では

ブラウザで使える .env と JSON の相互変換(ハシトシステム) は、2026-10-29 時点のコードで、引用符なしの値は最初の # の位置で切る(indexOf('#') で探してそこから先を落とす)。上の sample.env をツールと同じ関数で変換すると PASS1 は "abc"、URL は "https://example.com/"、COLOR は "" になり、Node.js / dotenv と同じ側の読み方だった。

Compose や python-dotenv で読む前提の .env をこのツールで JSON にすると、値が短く見えることがある。そのときは原文で引用符の有無を確かめてほしい。逆方向の JSON→.env では、#・空白・引用符などを含む値を自動で二重引用符で囲む。{"PASS":"abc#123","COLOR":"#ff0000"} は次のように出力され、4 つのパーサーのどれで読んでも元の値に戻る。

1
2
PASS="abc#123"
COLOR="#ff0000"

設定ファイルの値が読み手によって変わる別の例として、YAML の NO や on が false になる記事 も書いた。

まとめ

  • 引用符なしの abc#123 は、Node.js(--env-file / util.parseEnv / process.loadEnvFile)と dotenv では abc、python-dotenv と Docker Compose では abc#123 になる。COLOR=#ff0000 は前者で空文字列になる。
  • 差の原因は「# の前に空白が要るか」。Node.js と dotenv はどこでも、python-dotenv と Compose は空白の後だけをコメントとして扱う。
  • KEY= # comment や、タブの後の # は python-dotenv と Compose の間でも結果が違った。行内コメントは避け、コメントは独立した行に書く。
  • # を含む値は二重引用符か一重引用符で囲む。これで 4 つとも同じ値になる。

参考

  • Node.js ドキュメント: Environment Variables(DotEnv)、Command-line API(--env-file)
  • motdotla/dotenv README(Comments)
  • theskumar/python-dotenv README(File format)と dotenv/parser.py
  • Docker Docs: Set, use, and manage variables in a Compose file with interpolation(.env file syntax)

.envの#以降が消える Node・dotenv・Composeの差を実測
https://blog.hashito.biz/2026/10/29/dotenv-hash-inline-comment-node-env-file-python-dotenv-compose/
著者
hashito
作成日
2026年10月29日
著作権

このバージョンはその範囲に入るのか

依存の話でいちばん間違えやすいのは ^1.2.3 や ~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。