Docker コンテナに注入する環境変数、どれが優先される?適用のされ方を実際に調べてみた

最終的に Kubernetes 上で動かすつもりのアプリを作っていると、環境変数を注入してアプリに使用させることが多々ある。

Node.js 製のアプリでは、dotenv という npm パッケージで .env ファイルを読み込んだりできるので、開発環境ではコレを使って環境変数を擬似的に設定したりもする。

そんなアプリを Docker イメージ化して、開発環境で動作確認したいな、という時に、どうやって環境変数を注入するか、手段がいくつか存在する。

今回はそれらを確認し、どのように Docker コンテナに環境変数を注入できるか複数の手法を併用したらどの環境変数設定が優先されるのか、といったことをまとめていく。

目次

サンプルコード

今回は dotenv を使用した Node.js 製のアプリを想定し、簡単なサンプルコードを用意した。

{
  "name": "practice-env",
  "private": true,
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "dotenv": "8.2.0"
  }
}
const envFilePath = `${__dirname}/env-dir/.env`;
require('dotenv').config({ path: envFilePath });
setInterval(() => {
  console.log(`Env : [${envFilePath}]
    ENV_1 : [${process.env.ENV_1}]
    ENV_2 : [${process.env.ENV_2}]
    ENV_3 : [${process.env.ENV_3}]`);
}, 3000);
ENV_1=This is from .env file 1
ENV_2=This is from .env file 2
ENV_3=This is from .env file 3
FROM node:14-alpine3.12

WORKDIR /app/

# 環境変数を直接指定する
ENV ENV_1='From Dockerfile'
# dotenv で参照する環境変数ファイルをコピーする
COPY ./env-dir/.env             /app/env-dir/

COPY ./package.json ./index.js  /app/
RUN npm install

CMD ["npm", "start"]

ココまで用意できたら、

$ docker build -t practice-env:0 ./

というコマンドで Docker イメージをビルドしてやる。ココまでで環境変数に対して値を注入しているのは2ヶ所である。

  1. .env ファイルを dotenv で読み込む
  2. ENV で環境変数を設定する

準備はココまで。

環境変数の注入方法を知る

こうして作成した Docker イメージを元に、色々な方法で環境変数を注入していく。

その際、同じ名前の環境変数に対して、異なる値を注入しようとした時に、どのやり方で渡した値が優先されるのか、検証する。

優先順位は不等号記号で表現する。例えば A < B と書いたら、「A の方法で注入した値より、B の方法で注入した値の方が優先された」という表現になる。

1. そのまま実行 → .envENV

まずはそのまま実行してみる。

$ docker run --rm practice-env:0

出力を確認すると、COPY した .env ファイルを dotenv が読み込んでいることは確認できる。

しかし、環境変数 ENV_1 については、Dockerfile で ENV 命令にて設定した値の方が優先されていた。

dotenv は、既に値が設定されている場合は .env ファイルの内容を適用しないことが分かる。コレは実際のコードを見ても明らかである。

ということで、.env ファイルとシステム環境変数とでは、常にシステム環境変数の方が優先され、.env ファイルに書いても上書きはできないことを押さえておこう。

2. .env ファイルをボリュームマウントで差替 → イメージ内のファイル < ボリュームマウントしたファイル

次は docker run コマンド時に --volume (-v) オプションを使ってみる。

$ docker run --rm --volume="$(pwd)/docker-volume:/app/env-dir" practice-env:0

このようにして、Docker イメージに取り込み済みの .env ファイルを、別の内容の .env ファイルに差し替えてみる。コレをやるために、環境変数ファイル用のディレクトリ env-dir/ を設けていたワケ。

--volume オプションは、Docker イメージ内に既に同名のファイルがあっても、ディレクトリごと差し替える形で中身を入れ替えてしまう。というワケで、Docker イメージ内に COPY しておいた .env ファイルの内容は全て破棄され、ボリュームマウントした .env ファイルを dotenv が解釈する形となる。

3. --env-file オプション → dotenvENV 命令 < --env-file

次は、dotenv とよく似てはいるが、Docker 自身が指定のファイルをシステム環境変数として注入してくれる、--env-file オプションを使ってみる。

$ docker run --rm --env-file='./docker-env-file/ENV-FILE' practice-env:0

--env-file オプションは dotenv などとは関係なく、システム環境変数として注入されるので、シェルの世界で $ env コマンドを実行しても確認できるモノとなる。

このオプションを使うと、dotenv で注入した値は勿論、DockerfileENV 命令で定義した環境変数であっても、上書き適用できる。

4. --env オプション → dotenvENV 命令 < --env

次は .env ファイルを渡すのではなく、コマンドライン上で環境変数の Key・Value を渡してやる、--env (-e) オプションを使ってみる。

$ docker run --rm --env 'ENV_1=from env option`

コチラも --env-file と同様、dotenv で読み込む値、ENV 命令で指定した値よりも優先された。

全体的にいえるのは、

ということであろう。

オプションを併用したらどうなるか?

ココまでで大体分かってきた気はするが、もう少し検証。次は docker run 時に複数のオプションを併用した場合にどうなるか、見てみる。

1. ボリュームマウント + ファイルから注入 → ボリュームマウント < ファイルから注入

$ docker run --rm --volume="$(pwd)/docker-volume:/app/env-dir" --env-file='./docker-env-file/ENV-FILE' practice-env:0`

--volume--env-file。コレは dotenv 仕様のとおり、ボリュームマウントされた .env も読み込まれはするが、--env-file の値の方が優先される。

2. ボリュームマウント + オプション引数から注入 → ボリュームマウント < オプション引数から注入

$ docker run --rm --volume="$(pwd)/docker-volume:/app/env-dir" --env 'ENV_1=from env option' practice-env:0

--volume--env の比較。結果は --env-file の時と同じく、--env で指定した値の方が優先される。

3. ファイルから注入 + オプション引数から注入 → ファイルから注入 < オプション引数から注入

$ docker run --rm --env-file='./docker-env-file/ENV-FILE' --env 'ENV_1=from env option' practice-env:0

--env-file--env を併用した場合は、--env で指定した値の方が上書き優先される。

オプション引数の指定順は関係なく、--env-file よりも --env の方が優先された。コレは Docker の仕様どおりである。

これらの3つのフラグに関係なく、--env-file が始めに処理され、その後 -e--env フラグが処理されます。この方法は、必要な時に -e--env で変数を上書きするために使えます。

大部分は --env-file で指定したファイルから読み込ませたいが、一部だけ変えたい、という時に、ファイルを書き換えるのではなく実行時のコマンドで上書きできるようにしてあるワケだ。

4. ボリュームマウント + ファイルから注入 + オプション引数から注入 → ボリュームマウント < ファイルから注入 < オプション引数から注入

全てを併用した時も一応確認。

$ docker run --rm --volume="$(pwd)/docker-volume:/app/env-dir" --env-file='./docker-env-file/ENV-FILE' --env 'ENV_1=from env option' practice-env:0

それぞれの手法で、別々の名前の環境変数を設定しているのであれば、いずれも正しく適用される。

同じ名前の環境変数に対する設定が衝突する場合は、--volume より --env-file--env-file より --env の値の方が優先される。

まとめ

というワケで、Docker + dotenv における環境変数の適用順は、次の順番で順に定義され、上書きされていくモノと思うと良いだろう。

  1. dotenv が読み込む、Docker イメージ内の .env ファイル
  2. dotenv が読み込む、ボリュームマウントして差し替えた .env ファイル
    • 「1.」の .env ファイルと完全差し替え
    • 実際には「dotenv が指定した値が上書きされる」のではなく、「システム環境変数が存在したら dotenv が上書きしないように処理している」ワケだが…
  3. Dockerfile 中に書いた ENV 命令
  4. --env-file オプション
  5. --env オプション

dotenv による注入を抜きにすれば、ENV 命令 < --env-file--env の順で上書きされることを覚えておけば良い。

全体的には、「イメージ内に含まれるモノ」<「コマンド実行時の指定」 な順だが、その中でも --env-file--env は、より「その場限り感が強い方」の値を優先されるワケだ。

コレで覚えましたし。