はじめに
新卒研修にてコンテナアプリケーションを構築する機会がありました。
以前、Dockerfileを本番用とローカル用で分けている現場を見たことがあったため、今回はその是非も含めて「本番環境とローカル環境でDockerfileを分けるべきか」検討しました。
本記事は、その際に考えたことを備忘録として残しています。
結論として、本番環境とローカル環境で同一のDockerfileを作成しました。
環境ごとの差分は環境変数から注入するようにしました。
一つのDockerfileを使い回すことで、全体の構成もシンプルになりました。
どのように実現したかは以降の内容で説明します。
- 本記事では、frontendやbackendなど複数コンテナを一括で管理するために
docker runではなくdocker composeを使用します。 - ローカルの環境変数の出し分けは、
compose.ymlで行います。
想定読者
本記事の想定読者は以下です。
- Dockerfileを本番環境とローカル環境で使い分けるべきか悩んでいる方
- Dockerfileやコンテナの環境差分をどう管理すればよいか検討している方
技術スタック
- 言語:Typescript
- パッケージ管理ツール:npm
- フレームワーク
- フロントエンド:Next.js
- バックエンド:Hono
- クラウド:AWS
プロジェクト構造
今回はモノレポ構成でコード管理しています。
project_root
├── compose.yml # ローカル起動用(本番では使わない)
├── frontend/
│ ├── Dockerfile
│ ├── .env.example # frontendで使用する環境変数(環境変数のキー名はgit管理、値はダミー)
│ └── .env # 環境変数(frontend)のローカル実値(.gitignore対象)
└── backend/
├── Dockerfile
├── .env.example # backendで使用する環境変数(環境変数のキー名はgit管理、値はダミー)
└── .env # 環境変数(backend)のローカル実値(.gitignore対象)
全体の流れ
以降は以下の流れに沿って説明します。
- 今回解決したいこと
- 一つの
Dockerfileにまとめる利点と考慮事項 - どのように環境差分を外部から注入するか
今回解決したいこと
本記事で解決したいのは、次のような要件です。
- ローカル環境でだけ開発者向けの機能を有効にしたい(例:Next.jsの開発モード)
- ログの出力レベルを環境ごとに変えたい(例:ローカルは詳細に、本番は最小限に)
いずれも、アプリケーションの動作を環境ごとの値で切り替えたいという点で共通しています。
今回の技術スタックでは、前者はNODE_ENV、後者はLOG_LEVELという環境変数の値を切り替えることで実現します。
なお今回は、本番イメージのサイズをできるだけ小さくしたい、といった要件はありませんでした。
そのため「開発ツールを本番イメージから外すために分ける」という動機は最初から対象外とし、上記の「環境ごとに値を切り替えたい」という点だけにフォーカスしています。
一つのDockerfileにまとめる利点と考慮事項
すべての環境で一つのDockerfileを使用する利点(+)と考慮事項(-)
| # | 項目 | 内容 |
|---|---|---|
| 1 | + メンテナンス対象のファイルが減る |
ほぼ同一の内容が記載された別ファイルの管理が不要になる。 (ファイルの二重メンテナンスが不要になる) |
| 2 | + ビルド・起動コマンドを統一できる |
環境を問わず同じコマンドで動く状態を維持できます。 |
| 3 | + イメージを全環境で使い回せる |
環境変数に外出ししておけば、同じイメージを本番、ステージング、ローカルでそのまま使い回せます。 デプロイ時に環境変数の値を差し替えるだけで済みます。 |
| 4 | - 環境変数の外出しがほぼ必須 |
#3のメリットの裏返しとして#4の考慮が必要になる。 (詳細は後述) |
| 5 | - 変更の影響範囲が大きくなる |
ローカル用デバッグツールの追加や設定変更が、そのまま本番のイメージにも含まれます。 (例:ローカル専用のデバッグツールを追加すると、それが本番イメージにも入る) |
なぜ環境変数の外出しがほぼ必須になるのか
Dockerfileを一つにまとめると、環境ごとの差分を「ファイルを分けて書き分ける」ことができません。Dockerfileの中で値を持たせる手段は ENV だけになりますが、ENV で指定した値を変えるたびにイメージの再ビルドと再プッシュが必要になります。
たとえば ENV NODE_ENV=production と書いてしまうと、ローカルで development にしたいだけでもイメージを作り直すことになります。これでは「同じイメージを全環境で使い回す」という利点(#3)が崩れてしまいます。
そのため、環境ごとに変えたい値はDockerfileでは指定せず、実行時に外部から注入する(=環境変数として外出しする)のが、ほぼ必須の前提になります。次章では、具体的な注入方法を説明します。
どのように環境差分を外部から注入するか
ローカルでは環境変数を compose.yml から渡します。
これにより「共通のイメージを使用し、環境差分は外部から注入する」という状態になります。
ここで使うのが、Docker Composeの環境変数の優先順位です。
compose.ymlにおいて、environment は env_fileの内容よりも優先されます。[1]
下のレイヤほど優先度が低く、上のレイヤほど優先度が高く( = 最終的に効く)なります。
この性質を活用し、デフォルト設定をenv_fileで読み込み、ローカル限定の値だけenvironmentで上書きする構成にしています。
compose.ymlでの環境変数上書き例
この記事の冒頭で示したプロジェクト構造のうち、compose.ymlでは次のように記述したとします。
# compose.yml(抜粋)
services:
backend:
build: ./backend
env_file:
- ./backend/.env # ベース(本番想定の値も含む)
environment:
- NODE_ENV=development # ローカルだけ上書き
- LOG_LEVEL=DEBUG
各設定値のローカルと本番環境の環境変数の設定方法の出し分け
どの値をどこに置くかの役割分担は、シークレットかどうかで分けています。
| 項目 | ローカル指定方法 | ローカル以外(本番など)の指定方法 |
|---|---|---|
| シークレット(APIキーやDBのパスワードなど) |
.env(gitignore) |
AWS Secret Managerなど |
| 非機密の設定値(LOG_LEVELなど) |
environment で設定 |
AWS ECSのタスク定義など |
シークレットを environment に直接書いてしまうと、意図せずコード管理の対象になる。
まとめ
本記事では、本番環境とローカル環境でDockerfileを分けずに一つにまとめる方法と利点について紹介してきました。
一つにまとめれば同じイメージを全環境で使い回せる利点がある一方、ローカル向けの変更(開発ツールの追加など)がそのまま本番のイメージにも含まれるという考慮事項も伴います。
統一を検討する際は、利点と考慮事項の両方を見ておく必要があります。
今回、具体的に工夫したのは次の点です。
- 本番・ローカルでイメージを作り分けず、開発ツールも含んだ単一のイメージを使う構成にした。
- 環境ごとの挙動(開発モードやログレベル)は、
compose.ymlのenv_fileとenvironmentで、実行時に環境変数を注入して切り替えた。 - シークレットは
.env、非機密の設定値は.env.exampleとenvironmentで管理するようにした。
Dockerfileを一つにまとめることで、環境差分の管理に気を取られず、開発そのものに集中できるようになりました。
ここまで読んでいただきありがとうございました!
