Docker 中級 #5 環境変数と secrets 管理
ここまで環境変数と秘密値を適当に -e / environment: で流し込んできました。この記事はそのテーマだけ取り出して — どう注入し、どう露出させないか を本格的に整理します。
Docker 中級 シリーズでこの記事の位置:
- #1 マルチステージビルドとイメージスリミング
- #2 ビルドキャッシュ — レイヤー順序の最適化
- #3 docker compose 基礎 — web + db
- #4 compose 深掘り — depends_on, healthcheck, profiles
- #5 環境変数と secrets 管理 ← この記事
- #6 ロギングとデバッグ
環境変数の入り口 #
Docker で環境変数がコンテナに届く経路は意外に複数あります。一箇所に整理すると:
┌─────────────────────────┐
[Dockerfile] │ │
ENV KEY=value ────────▶ │ │
│ │
[docker run] │ コンテナの中の │
-e KEY=val ──────▶ │ プロセスの │
--env-file file ──────▶ │ KEY 環境変数 │
│ │
[compose.yaml] │ │
environment: ──────▶ │ │
env_file: ──────▶ │ │
│ │
[host shell] │ │
$KEY (補間) ──────▶ │ │
└─────────────────────────┘同じ変数が複数の箇所で定義されると 後から入ってきたものが勝ちます。compose の environment: > env_file: > Dockerfile の ENV の順で優先されます。
これを頭に入れておけば「環境変数が反映されません」の 9 割は追跡が終わります。
.env ファイル — 最も典型的な入り口
#
同じディレクトリに .env ファイルがあると Compose が自動で読み取って、compose.yaml の中の ${VAR} 補間 に使います。
POSTGRES_PASSWORD=secret
APP_VERSION=1.0.0
DB_HOST=pgservices:
pg:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
web:
image: myapp:${APP_VERSION}
environment:
DB_HOST: ${DB_HOST}
DB_PASSWORD: ${POSTGRES_PASSWORD}ここで押さえておく二つの事実:
- この
.envは compose ファイル自体の補間に使われる。コンテナの中に自動注入されるわけではありません。 - コンテナにその値が届くようにするには
environment:/env_file:で明示する必要があります。
変数補間の文法 #
${VAR} # 単純参照。なければ空文字列
${VAR:-default} # VAR がないか空文字列なら default
${VAR-default} # VAR が定義されていないときだけ default (空文字列はそのまま)
${VAR:?error message} # VAR がないか空文字列ならエラー
${VAR?error message} # VAR が定義されていなければエラー運用でよく使うパターン:
services:
pg:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}.env が抜けたまま compose up をすると即座にエラーで知らせるので、空のパスワードで立ち上がってしまう事故を防ぎます。
environment: vs env_file:
#
サービスに変数を注入する二つの方法。
services:
web:
environment:
DEBUG: "1"
DB_HOST: pg
DB_PASSWORD: ${POSTGRES_PASSWORD}services:
web:
env_file:
- .env.web
- .env.localDEBUG=1
DB_HOST=pg
LOG_LEVEL=info| environment | env_file | |
|---|---|---|
| 定義位置 | compose.yaml の中 | 別ファイル |
| 変数の数が多いとき | 長くなる | きれい |
| 補間 | 可能 (${VAR}) | 補間なし — リテラル |
| 優先順位 | env_file より高い | environment より低い |
複数の env_file を書けば後ろが前を上書きし、その後 environment: が全てを上書きします。この優先順位だけ頭に入れれば混乱しません。
よくある混同:
env_fileの中でKEY=${OTHER_KEY}のような補間は 動きません。env_fileは単純な dotenv ファイルなので全ての値がリテラルで読まれます。補間が必要ならenvironment:で。
変数優先順位一表 #
同じ変数が複数の箇所にあるとき、結果は最も上が勝ちます。
| 順位 | 出所 |
|---|---|
| 1 (最強) | docker compose run -e KEY=val (CLI 明示) |
| 2 | compose.yaml の environment: |
| 3 | compose.yaml の env_file: |
| 4 | ホストシェルの環境変数 (compose 開始時) |
| 5 | .env ファイル (compose.yaml 補間用 — コンテナへ直接注入ではない) |
| 6 | Dockerfile の ENV |
docker compose config でマージされた結果を見れば混乱が早く解けます。
秘密値と環境変数 — どこまで OK か #
DB パスワード、API キー、JWT シークレット — このような値を環境変数で渡すのはよくありますが、セキュリティレベルによって 違う道具が必要になります。
| レベル | 適切な道具 |
|---|---|
| 開発 / ローカル | .env ファイル (コミット禁止、.gitignore) |
| CI / 小さな運用 | CI の secret store、compose の environment: |
| 運用 (適度) | compose secrets: または K8s Secret |
| 運用 (高い) | AWS Secrets Manager、Vault、GCP Secret Manager のような外部マネージャ |
この記事は最初の二段階に集中して、後者は最後に一段落で触れます。
.env のセキュリティ — .gitignore が最初の行
#
ほとんどの秘密漏洩は単純なミスから来ます。
.env
.env.*
!.env.example.env.example はキー名だけ書かれたテンプレート。新しい人がクローンするときどんな値を埋めるべきかの案内書です。
POSTGRES_PASSWORD=
DJANGO_SECRET_KEY=
SENTRY_DSN=POSTGRES_PASSWORD=actual-secret
DJANGO_SECRET_KEY=actual-key
SENTRY_DSN=https://...@sentry.io/...このパターンが定着すると同僚に秘密を別途教える必要がなくなり、「環境変数を除く他のセットアップは OK?」のようなデバッグも速くなります。
もう一つ — pre-commit で漏洩防止
#
gitleaks、detect-secrets のような道具を pre-commit フックに入れれば、誤って秘密が入ったコミットを事前にブロックできます。一度漏洩した秘密は git history からきれいに消すのがとても面倒です (git filter-branch または BFG Repo-Cleaner)。事前ブロックが一番安い。
環境変数の露出 — どこから漏れるか #
秘密が環境変数で入ってきたからといって自動的に安全なわけではありません。Docker 環境では次のところから漏れやすいです。
docker inspect
#
docker inspect myapp-web-1 --format '{{json .Config.Env}}'
# ["PATH=...", "DB_PASSWORD=secret", ...]コンテナにアクセス可能なユーザならこの値をそのまま見られます。ホストセキュリティの一部です。
docker history
#
ビルド時に環境変数が刻まれたイメージは docker history で露出します。
docker history myapp:1.0
# CREATED BY SIZE
# ENV DB_PASSWORD=secret 0B ← 永遠に刻まれるイメージには絶対に秘密を刻まないでください。 イメージを受け取った全員が平文で見られて、レジストリにプッシュすればさらに広がります。
プロセスツリー #
docker exec myapp-web-1 env | grep PASSWORD
docker exec myapp-web-1 cat /proc/1/environ | tr '\0' '\n'コンテナ内部にシェルで入れる権限があれば環境変数は全て見えます。だからコンテナの中へのシェルアクセス権限自体を制御するのが一段の防御線です。
Compose secrets: — 一段上の道具
#
環境変数より安全な秘密注入方式。secrets: は秘密値を ファイル としてコンテナの中にマウントします。環境変数ではありません。
services:
pg:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: app
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtsuper-secret-passwordsecrets/このパターンの利点:
- 秘密が 環境変数で露出しない —
inspectで見えない /run/secrets/db_passwordという 読み取り専用ファイル としてマウント- Postgres のような公式イメージはすでに
*_FILE環境変数の慣習をサポートしている
外部 secret — Swarm モード用 #
secrets:
db_password:
external: trueexternal: true は Compose が secret を作らずすでに存在する secret を参照するという意味で、これは Swarm モードで真価を発揮します。普通の compose up では file 形式が日常的です。
ビルド時秘密 — --mount=type=secret
#
#2 で一度触れた BuildKit シークレット。ビルド中だけ必要な秘密 (例: プライベートパッケージレジストリのトークン) を扱うとき。
# syntax=docker/dockerfile:1.7
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=pypi_token \
pip install --extra-index-url \
https://__token__:$(cat /run/secrets/pypi_token)@pypi.example.com/simple \
-r requirements.txtservices:
web:
build:
context: .
secrets:
- pypi_token
secrets:
pypi_token:
file: ./secrets/pypi_token.txtビルド中だけ /run/secrets/pypi_token として読まれて — イメージのどのレイヤーにも残りません。 docker history でも見えません。プライベートパッケージインデックス / GitHub Personal Access Token / 社内ミラーの認証のようなケースで定石です。
外部 secret manager — 一段落 #
運用規模が大きくなると秘密をファイルや環境変数で持っていること自体が負担です。クラウド環境では外部マネージャに移します。
| 環境 | 慣用ツール |
|---|---|
| AWS | Secrets Manager、Parameter Store |
| GCP | Secret Manager |
| Azure | Key Vault |
| Self-hosted | HashiCorp Vault、Bitnami Sealed Secrets |
| K8s | External Secrets Operator + 上のマネージャ |
流れはおおむね次のとおりです。コンテナが起動時にマネージャから秘密を取得し、環境変数またはファイルとして自分自身に注入します。イメージに秘密がなく、コンテナ定義にも秘密がなく、マネージャの権限さえうまく制御すれば、漏洩面がとても小さくなります。
Docker / Compose 単独環境ではここまで行くことは少ないですが、運用が ECS / EKS / GKE に行けば自然に出会う場面です。
よく出会うミス #
- イメージに
ENV API_KEY=...を刻む —docker historyに平文で永遠に。ビルドシークレット / ランタイム注入で。 .envを git コミット — 一度 push されると history から外すのがとても面倒。pre-commit gitleaks が一番安い防御。docker compose logsが秘密を出力 — アプリが起動時に環境変数全体をログに吐くパターン (一部のライブラリは標準でそう)。マスキングまたは出力オフ。- ホストシェルの変数が意図せずコンテナに入る —
environment: - MY_VAR(値なし) 形式はホスト環境から取ります。意図がなければ明示値で。 - secret ファイルの権限 —
secrets/ディレクトリはchmod 600で締めておく方が安全。
まとめ #
この記事で掴んだ絵:
- コンテナの中の環境変数の出所は 6 種類。compose CLI >
environment:>env_file:> ホストシェル >.env補間 > DockerfileENVの順で勝つ .envは compose.yaml 補間用。コンテナに直接行くわけではありません。.gitignore+.env.exampleのセットアップが基本- イメージに秘密を刻まないこと —
docker historyで露出。ENVで書かない - Compose
secrets:は環境変数ではなく 読み取り専用ファイル で秘密注入 —*_FILE慣習とよく合う - ビルド時秘密は BuildKit
--mount=type=secret— どのレイヤーにも残らない - 運用規模が大きくなれば外部 secret manager (AWS Secrets Manager、Vault など) へ
次の記事 (#6 ロギングとデバッグ) では中級シリーズを締めくくります。複数コンテナのログを一箇所で見て、log driver を変更し、docker compose logs のよく使うオプション、そしてコンテナの中のデバッグ道具 (exec、inspect、stats、dive) までです。