Docker 中級 #5 環境変数と secrets 管理

読了 8分

ここまで環境変数と秘密値を適当に -e / environment: で流し込んできました。この記事はそのテーマだけ取り出して — どう注入し、どう露出させないか を本格的に整理します。

Docker 中級 シリーズでこの記事の位置:

環境変数の入り口 #

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} 補間 に使います。

.env
POSTGRES_PASSWORD=secret
APP_VERSION=1.0.0
DB_HOST=pg
compose.yaml
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  web:
    image: myapp:${APP_VERSION}
    environment:
      DB_HOST: ${DB_HOST}
      DB_PASSWORD: ${POSTGRES_PASSWORD}

ここで押さえておく二つの事実:

  1. この .env は compose ファイル自体の補間に使われる。コンテナの中に自動注入されるわけではありません。
  2. コンテナにその値が届くようにするには 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: #

サービスに変数を注入する二つの方法。

environment — インライン
services:
  web:
    environment:
      DEBUG: "1"
      DB_HOST: pg
      DB_PASSWORD: ${POSTGRES_PASSWORD}
env_file — ファイルから
services:
  web:
    env_file:
      - .env.web
      - .env.local
.env.web
DEBUG=1
DB_HOST=pg
LOG_LEVEL=info
environmentenv_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 明示)
2compose.yamlenvironment:
3compose.yamlenv_file:
4ホストシェルの環境変数 (compose 開始時)
5.env ファイル (compose.yaml 補間用 — コンテナへ直接注入ではない)
6Dockerfile の 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 が最初の行 #

ほとんどの秘密漏洩は単純なミスから来ます。

.gitignore — 必ず入れるべき行
.env
.env.*
!.env.example

.env.example はキー名だけ書かれたテンプレート。新しい人がクローンするときどんな値を埋めるべきかの案内書です。

.env.example (コミット OK)
POSTGRES_PASSWORD=
DJANGO_SECRET_KEY=
SENTRY_DSN=
.env (コミット禁止)
POSTGRES_PASSWORD=actual-secret
DJANGO_SECRET_KEY=actual-key
SENTRY_DSN=https://...@sentry.io/...

このパターンが定着すると同僚に秘密を別途教える必要がなくなり、「環境変数を除く他のセットアップは OK?」のようなデバッグも速くなります。

もう一つ — pre-commit で漏洩防止 #

gitleaksdetect-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: は秘密値を ファイル としてコンテナの中にマウントします。環境変数ではありません。

compose.yaml — 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.txt
./secrets/db_password.txt
super-secret-password
.gitignore
secrets/

このパターンの利点:

  • 秘密が 環境変数で露出しないinspect で見えない
  • /run/secrets/db_password という 読み取り専用ファイル としてマウント
  • Postgres のような公式イメージはすでに *_FILE 環境変数の慣習をサポートしている

外部 secret — Swarm モード用 #

external secret (Swarm)
secrets:
  db_password:
    external: true

external: true は Compose が secret を作らずすでに存在する secret を参照するという意味で、これは Swarm モードで真価を発揮します。普通の compose up では file 形式が日常的です。

ビルド時秘密 — --mount=type=secret #

#2 で一度触れた BuildKit シークレット。ビルド中だけ必要な秘密 (例: プライベートパッケージレジストリのトークン) を扱うとき。

Dockerfile — ビルドシークレット
# 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.txt
compose.yaml — build secret
services:
  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 — 一段落 #

運用規模が大きくなると秘密をファイルや環境変数で持っていること自体が負担です。クラウド環境では外部マネージャに移します。

環境慣用ツール
AWSSecrets Manager、Parameter Store
GCPSecret Manager
AzureKey Vault
Self-hostedHashiCorp Vault、Bitnami Sealed Secrets
K8sExternal 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 補間 > Dockerfile ENV の順で勝つ
  • .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 のよく使うオプション、そしてコンテナの中のデバッグ道具 (execinspectstatsdive) までです。

X