DockerでJavaコンテナを構築する10のベストプラクティス
2022年8月24日
0 分で読めます編集者注
この記事は2021年2月18日に初公開されましたが、最新の情報を反映するために更新されています。
Javaアプリケーションを構築して、Dockerイメージ内で実行したいとお考えですか?その方法をご紹介します。
このチートシートでは、本番環境に適したJavaコンテナを構築するためのベストプラクティスを紹介します。ここで紹介するガイドラインに沿ってJavaコンテナを構築し、アプリケーション向けに最適化された安全なコンテナを作成していきます。このチートシートは、DockerでJavaコンテナを作成するときだけでなく、ほかの人のコードをレビューするときにも役立つガイドです。どちらの場合も、本番環境にデプロイするDockerイメージの最適化とセキュリティ確保に注力することが重要です。

10のベストプラクティス
Dockerのベースイメージには、明示的で再現性のあるタグを指定する(「latest」はバージョンではありません!)
Javaコンテナイメージには、本番環境で必要なものだけをインストールする
Java Dockerイメージのセキュリティ脆弱性を見つけて修正する
マルチステージビルドを使って本番イメージをさらに小さくする
Javaアプリをrootユーザーで実行しない
イベントを適切に処理してJavaアプリケーションを安全に終了する
Javaアプリケーションを適切にシャットダウンする
.dockerignoreを使って、不要なファイルをJavaコンテナイメージに含めないJavaがコンテナを認識できるようにする
Dockerコンテナの自動生成ツールの使用には注意する
開発者ファーストのコンテナセキュリティ
Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。
Javaコンテナイメージの構築で避けるべきこと
まずは、Mavenで作成したJavaアプリケーション用のシンプルなDockerfileから始めましょう。Javaコンテナの構築に関する多くの記事で、次の例のような内容が紹介されています。
多くのブログ記事では、Java Dockerイメージを構築するための基本的なDockerfileの手順を紹介して終わっています。
これをDockerfileという名前で保存し、ビルドして実行します。
シンプルで、問題なく動作します。しかし、このイメージには多くの問題があります。Mavenの適切な使い方を知る必要があるだけでなく、上記の例のようなJavaコンテナの構築は何としても避けるべきです。
このDockerfileを段階的に改善、最適化し、Javaアプリケーション向けの効率的で安全なDockerイメージを作成しましょう。
1. Dockerのベースイメージには、明示的で再現性のあるタグを指定する
MavenでJavaコンテナイメージを構築する場合、Mavenイメージをベースにするのが当然のように思えるかもしれません。Docker Hubから簡単に取得して、すぐに使い始められます。しかし、このベースイメージを使うと、実際に何が取り込まれるのか把握できているでしょうか?Dockerイメージはタグで指定できますが、タグを指定しないと暗黙的にlatestタグが使われます。
便利な機能に思えるかもしれませんが、デフォルトのMavenイメージを使う方法には、次のような問題が潜んでいます。
便利な機能に思えるかもしれませんが、デフォルトのMavenイメージを使う方法には、次のような問題が潜んでいます。
Dockerのビルド結果に一貫性がなくなる
つまり、再ビルドすると結果が大きく変わる可能性があります。今日のlatestイメージは、明日や来週のlatestイメージとは異なるかもしれません。ベースにするMavenやJDKのバージョンが更新されると、アプリケーションのバイトコードが変わり、予期しない結果につながる可能性があります。イメージを再ビルドする際には、再現可能で一貫した動作が求められます。
Maven DockerイメージはフルOSイメージをベースにしている
そのため、最終的な本番イメージに多数の追加バイナリが含まれてしまいます。その多くは、アプリケーションの実行には不要です。Javaコンテナイメージに含めると、次のような問題が発生します。
イメージサイズが大きいほど、ダウンロードや再ビルドに時間がかかる。
追加のバイナリがセキュリティ脆弱性をもたらす可能性がある。脆弱性を含むバイナリはすべて、システムに持ち込みたくない潜在的なセキュリティリスクとなる。
maven:latestイメージはかなり大きく、MavenやOpenJDKのバージョンが数か月、数週間、場合によっては数日で変わるものをベースにしています。
次の方法で、この問題を軽減できます。
ニーズを満たす範囲で、できるだけ小さいベースイメージを使いましょう。考えてみてください。プログラムの実行に、追加のバイナリをすべて含むフルOSが必要でしょうか?そうでなければ、
debian-slimやalpineのような小さなイメージでも、フルサイズのDebianイメージと同じように使えるかもしれません。イメージを具体的に指定しましょう。特定のイメージバージョンを使えば、動作をある程度制御し、予測できます。
maven:3.6.3-jdk-11-slimを使えば、JDK 11とMaven 3.6.3を使用していることが明確になります。JDKの6か月ごとの更新サイクルがJavaコンテナの動作に影響することもありません。さらに厳密に指定するには、イメージのSHA256ハッシュを使用できます。ハッシュを指定すれば、イメージを再ビルドするたびに、まったく同じベースイメージが使われます。
この知識を踏まえて、Dockerfileを更新しましょう。
安全なコンテナイメージの選び方について詳しくは、コンテナセキュリティガイドをご覧ください。
2. Javaコンテナイメージには、本番環境で必要なものだけをインストールする
次のコマンドは、すべての依存関係を含めてJavaプログラムをコンテナ内でビルドします。つまり、ソースコードとビルドシステムの両方が、本番用Javaコンテナに含まれてしまいます。
Javaはコンパイル言語です。つまり、必要なのはビルド環境で作成された成果物だけで、コード自体は必要ありません。また、ビルド環境を本番コンテナに含める必要もありません。
Javaイメージの実行にフルJDKは必要なく、JREで十分です。つまり、実行可能なJARの場合、JREとコンパイル済みのJava成果物を含むイメージを構築すればよいのです。
Maven(またはGradle)を使ってCIパイプラインでプログラムをビルドし、次の更新後のDockerfileのように、JARを本番イメージにコピーします。
3. JavaコンテナのDockerイメージのセキュリティ脆弱性を見つけて修正する
Docker Javaコンテナにはすでに小さなイメージを使っています。しかし、このベースイメージに含まれるバイナリに問題がないかは分かりません。Snyk CLIを使ってDockerイメージをテストしましょう。一緒に試すには、Snykの無料アカウントに登録してください。
CLIはnpm、brew、scoopを使ってインストールするか、GitHubから最新のバイナリをダウンロードできます。ここではnpmでインストールし、無料アカウントで認証します。
snyk container testを使うと、任意のDockerイメージをテストできます。Dockerfileを追加すると、より適切な修正アドバイスも得られます。

Snykは、このベースイメージに58件のセキュリティ問題を検出しました。そのほとんどは、Debian Linuxディストリビューションに含まれるバイナリに関するものです。
この結果を踏まえ、ベースイメージをadoptopenjdkが提供するAlpineベースのopenjdk11:JREイメージに切り替え、より明確に指定するため、特定のSHAも指定します。
このバージョンをsnyk containerでテストすると、このベースイメージに既知の脆弱性は見つかりませんでした。
同様に、プロジェクトのルートでsnyk testを実行すれば、Javaアプリケーションをテストできます。ローカルマシンで開発する際には、アプリケーションと作成したJavaコンテナイメージの両方をテストすることをおすすめします。さらに、CIパイプラインでアプリケーションとイメージの両方を同じように自動テストしましょう。
また、新たな脆弱性は時間とともに発見されることを忘れないでください。新しい脆弱性が特定されたら、通知を受け取りたいはずです。
アプリケーションにはsnyk monitorを、Dockerイメージにはsnyk container monitorを使うと、脆弱性の通知を受け取れます。現在本番環境で使っているバージョンを監視しておけば、新たなセキュリティ問題が見つかったときに適切に対応できます。

また、GitリポジトリをSnykに接続すれば、SDLCのその段階で脆弱性を見つけ、修正できるよう支援します。
現在のDockerfileを更新しましょう。
詳しくは、Dockerコンテナの脆弱性スキャンに関するガイドをご覧ください。
4. Javaコンテナにマルチステージビルドを使う
この記事では先ほど、Javaアプリケーションをコンテナ内でビルドする必要はなく、ビルドの成果物だけが必要だと説明しました。ただし、Dockerイメージのビルドプロセスの一部としてアプリケーションをビルドすると便利な場合もあります。
幸い、Dockerイメージの作成は複数のステージに分けられます。アプリケーションのビルドに必要なツールをすべて含むビルドイメージを作成し、次のステージでは、本番環境で実行するために必要なものだけを含む本番イメージを作成できます。
機密情報の漏えいを防ぐ
JavaアプリケーションやDockerイメージを作成する際、プライベートなMavenリポジトリへの接続が必要になることがあります。通常、認証情報はローカルマシンやビルド環境のsettings.xmlに保存します。マルチステージビルドなら、settings.xmlをビルド用コンテナに安全にコピーできます。認証情報を含む設定ファイルが本番イメージに入ることはありません。コマンドライン引数に認証情報が必要な場合も、ビルドイメージ内で安全に使用できます。本番イメージには含まれません。
マルチステージビルドでは複数のステージを作成し、その成果物だけを最終的な本番イメージにコピーできます。コードのコンパイルとイメージの構築を分けることで、アプリケーションの実行に必要なものだけを最終的なコンテナイメージにデプロイでき、ソースコードやコンパイラなどがデプロイ先の環境に漏れるのを防げます。
ちなみに、作成したJavaコンテナイメージに対してdocker historyを実行した結果を見てみましょう。
出力にはビルドイメージではなく、本番コンテナイメージの情報だけが表示されています。

5. コンテナをrootユーザーで実行しない
Dockerコンテナを作成するときは、Javaコンテナのセキュリティ対策として最小権限の原則を適用しましょう(つまり、必要な権限だけを与えます)。何らかの理由で攻撃者がアプリケーションに侵入しても、すべてにアクセスできる状態にはしたくありません。
セキュリティ対策を複数の層に設けることで、システムが侵害された際に攻撃者が与えうる被害を軽減できます。そのため、アプリケーションをrootユーザーで実行していないことを必ず確認してください。
問題は、Dockerコンテナを作成すると、デフォルトではrootユーザーとして実行されることです。開発時には便利でも、本番イメージでは避けるべきです。何らかの理由で攻撃者がターミナルにアクセスしたり、コードを実行したりできるとします。その場合、実行中のコンテナに対する強い権限を得るだけでなく、アクセス権が過剰なファイルシステムのバインドマウントを通じて、ホストのファイルシステムにアクセスされる可能性もあります。
解決方法は簡単です。ベースイメージのドキュメントを確認し、権限の低いユーザーが含まれていれば、そのユーザーを指定したUSER行をDockerfileに追加して使います。含まれていない場合は、アプリケーションを実行するための権限を制限した専用ユーザーを作成し、それを使います。そのユーザーでアプリケーションを実行できることを必ずテストしてください。
それに合わせてDockerfileを更新しましょう。マルチステージコンテナビルドの2つ目のステージでのみ、ユーザーを指定している点に注目してください。
注:別のユーザーとして実行するには、コマンドラインやオーケストレーターの設定でユーザーを指定する方法もあります。たとえば、Docker CLIのrunコマンドには、この目的で使える-uパラメーターがあります。KubernetesのPod仕様でもSecurityContext:runAsUserフィールドで同じ設定ができます。ただし、ビルド仕様に指定しておけば、繰り返し再現できます。
6. イベントを適切に処理してJava Dockerウェブアプリケーションを安全に終了する
多くの例で、コンテナ化されたJavaアプリケーションの起動にビルド環境を使う、よくある間違いを見かけます。
MavenやGradleをJavaのDockerコンテナに含めるべき理由についてはすでに説明しましたが、これらを避けるべき理由はほかにもあります。
CMD “mvn” “exec:java”CMD [“mvn”, “spring-boot run”]CMD “gradle” “bootRun”CMD “run-app.sh”
Dockerでアプリケーションを実行すると、最初のアプリケーションはプロセスID 1(PID 1)として実行されます。LinuxカーネルはPID 1を特別に扱います。通常、PID 1のプロセスはinitプロセスです。Mavenを使ってJavaアプリケーションを実行する場合、MavenがSIGTERMのようなシグナルをJavaプロセスに転送することをどうすれば確認できるでしょうか?できません。
代わりに、以下の例のようにDockerコンテナを実行すれば、JavaアプリケーションがPID 1となり、シグナルが正しく渡されます。
docker kill ...コマンドとdocker stop ...コマンドは、PID 1のコンテナプロセスにのみシグナルを送信します。Javaアプリケーションを実行するシェルスクリプトを実行している場合は、/bin/shなどのシェルは子プロセスにシグナルを転送しない点に注意してください。そのため、アプリケーションはSIGTERMを受け取れません。
Linuxでは、PID 1にはさらにいくつかの責任があることも知っておきましょう。詳しくは、こちらの記事DockerとPID 1のゾンビプロセス回収問題で分かりやすく説明されています。こうした問題への対処法が分からない場合など、PID 1になりたくないケースもあります。その場合はdumb-initを使うのが有効です。
このようにDockerコンテナを実行すると、dumb-initがPID 1となり、必要な責任をすべて担います。Javaプロセスがこれらに対処する必要はなくなります。
更新後のDockerfileは次のようになります。
7. Java Webアプリケーションを安全に終了する
アプリケーションがシャットダウンシグナルを受け取ったら、すべてを適切に終了させるのが理想です。アプリケーションの開発方法によっては、中断シグナル(SIGINT)やCTRL + Cによってプロセスが即座に終了することがあります。
これは望ましくない場合があります。予期しない動作や、データの損失につながる可能性があるためです。
PayaraやApache TomcatなどのWebサーバー上でアプリケーションを実行している場合、通常はWebサーバーが適切なシャットダウンを処理します。実行可能なアプリケーションを構築するための一部のフレームワークも同様です。たとえばSpring BootにはTomcatが組み込まれており、シャットダウンを適切に処理します。
スタンドアロンのJavaアプリケーションを作成したり、実行可能なJARを手動で作成したりする場合は、こうした中断シグナルを自分で処理する必要があります。
解決方法は簡単です。以下の例のように、ランタイムにシャットダウンフックを追加できます。SIGINTのようなシグナルを受け取ると、新しいThreadが起動し、適切なシャットダウンを処理します。
厳密には、これはDockerfileよりもWebアプリケーション全般に関わる問題ですが、オーケストレーション環境では特に重要です。詳しくは、コンテナオーケストレーションに関するガイドをご覧ください。
8. .dockerignoreを使ってJavaコンテナイメージから不要なファイルを除外する
(公開されている)Gitリポジトリ内の特定のファイルを除外するには、.gitignoreファイルを使えます。不要なファイルがGitリポジトリに混入するのを防ぎます。また、機密ファイルが公開リポジトリに漏れるのを防ぐのにも役立ちます。
Dockerイメージにも、同様の.dockerignoreファイルがあります。Gitのignoreファイルと同じように、指定したパターンに一致するファイルを無視し、不要なファイルやディレクトリがDockerイメージにコピーされるのを防ぎます。
仕組みを説明すると、docker buildコマンドを実行すると、CLIツールはビルドコンテキストのディレクトリ(デフォルトではDockerfileを含むディレクトリ)の内容をコンテナランタイムに送信します。このコピーが、Dockerfileで定義された各ステップで使われます。.dockerignoreのパターンに一致するファイルはコピーされず、ビルド時にCOPYやADDコマンドで取り込むこともできません。.gitのような大きなディレクトリツリーを除外すると、コンテキストのコピーが大幅に速くなり、ビルド時間を短縮できます。ネットワーク経由で別のサーバー上のDockerエンジンを使ってビルドする場合は、特に効果があります。
この記事の簡略化した例では、単一の実行可能JARを使うことを前提としています。Javaアプリケーションの提供方法はこれだけではありません。スタンドアロンのJavaアプリケーションでは、すべての依存関係をアーティファクト内に含めたfat JARを作成しない場合もあります。状況によっては、アプリケーションが依存するJARを含むディレクトリ全体や、ほかのファイルをコピーする必要があります。機密情報が誤ってDockerイメージに紛れ込むことは避けなければなりません。特に、イメージを一般公開する場合は要注意です。
.dockerignoreの例を見てみましょう。
.dockerignoreファイルを使うメリットは次のとおりです。
テスト目的でのみ使う依存関係を除外できます。
.envやaws.jsonファイルに含まれる認証情報などのシークレットがJavaのDockerイメージに入り込むのを防げます。デバッグログファイルにも、公開したくないシークレットや機密情報が含まれる場合があるため注意してください。Dockerイメージを整理してサイズを小さくできます。また、予期しない動作の防止にも役立ちます。
コピーが必要なビルドコンテキストディレクトリのサイズを縮小し、docker buildの処理を高速化できます。
9. Javaがコンテナを認識できるようにする
Java仮想マシン(JVM)は優れた仕組みです。実行環境に応じて自らを調整します。動作状況に基づく調整によって、ヒープサイズを動的に最適化します。しかし、Java 8やJava 9などの古いバージョンでは、JVMはコンテナに設定されたCPUやメモリの上限を認識しませんでした。これらのJavaバージョンのJVMは、ホストシステムで利用できるすべてのメモリとCPUを認識していました。つまり、Dockerグループの設定は無視されていたのです。
Java 10のリリース以降、JVMはコンテナを認識し、コンテナに設定された制約を把握できるようになりました。UseContainerSupport機能はJVMフラグで、デフォルトで有効になっています。Java 10でリリースされたこのコンテナ認識機能は、Java 8u191にもバックポートされています。
Java 8より古いバージョンでは、-Xmxフラグを使ってヒープサイズを手動で制限できますが、かなり手間がかかります。また、ヒープサイズはJavaが使用するメモリ量と同じではありません。Java 8u131とJava 9では、コンテナ認識機能は実験的な機能です。実験的なJVMオプションとメモリ制限フラグを有効にする必要があります。
コンテナサポートをデフォルトで有効にするには、Java 10以降の新しいバージョンにアップデートするのが最善です。残念ながら、多くの企業はいまだにJava 8に大きく依存しています。そのため、Dockerイメージ内のJavaをより新しいバージョン(できれば最新のLTSバージョン)に更新するか、少なくともJava 8u191以降を使うようにしてください。
Javaコンテナのセキュリティとは関係ないように聞こえるかもしれません。しかし、可用性は情報セキュリティのCIAトライアドを構成する3つの要素の1つです。
10. Dockerコンテナの自動生成ツールには注意する
インターネットで情報を探していると、ビルドシステム向けの優れたツールやプラグインが見つかるでしょう。JavaのDockerコンテナを作成し、必要に応じて自動で公開までできる便利なツールもあります。
開発者にとっては、アプリケーションの作成に加えてDockerfileの保守に気を配る必要がなくなるため、非常に魅力的に見えます。
そのようなプラグインの例としてJIBがあります。以下のようにビルドプラグインを設定して、mvn jib:dockerBuildを実行するだけです。
手間をかけずに、指定した名前のDockerイメージをビルドできます。
Spring Bootのバージョン2.3以降でも、mvnのターゲットを実行すれば同様のことができます。
どちらの場合も、システムがJavaのDockerコンテナイメージを自動で作成します。これらのコンテナは比較的小さいことも確かです。イメージのベースにeclipse-temurinやbuildpacksを使っているためです。しかし、イメージのサイズにかかわらず、コンテナが安全かどうかをどう確認できるでしょうか?詳しく調査する必要がありますが、それでも今後も安全な状態が維持される保証はありません。
どちらのイメージもsnyk containerでスキャンすると、2つのシステムが使用しているベースイメージに関連する脆弱性がいくつか見つかります。また、ここですでに説明したほかの項目とあわせて、ユーザー権限が正しく設定されているかどうかも確認が必要です。
JavaのDockerイメージを作成する際に、こうしたツールを使うべきではないと言っているわけではありません。ただし、イメージを公開する予定があるなら、Javaのコンテナセキュリティのあらゆる側面を適切に調査してください。まずはコンテナをスキャンするのがよいでしょう。別のツールを使う場合は、ベースイメージやデフォルトユーザーなどのデフォルト設定を調査し、アプリケーションに適した設定に上書きしてください。Dockerfileに明示的に記述するのと同じように、こうした設定を明示することが常に安全な選択です。
まとめ
これで、JavaアプリケーションをDockerコンテナに安全に収めるためのガイドは終了です。パフォーマンスとセキュリティに関する最適化を考慮し、本番環境に対応できるJavaのDockerイメージを構築する方法を紹介しました。
ぜひ確認していただきたい関連リソースをご紹介します。
Jibを使ったJavaコンテナイメージの構築

開発者ファーストのコンテナセキュリティ
Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。
よくある質問
Dockerコンテナ内のJavaのバージョンを確認するにはどうすればよいですか?
DockerコンテナイメージにインストールされているJavaのバージョンを確実に確認するには、コンテナ内でjava -versionコマンドを実行します。
docker run --rm -it eclipse-temurin:11 java -version
openjdk version "11.0.16" 2022-07-19
OpenJDK Runtime Environment Temurin-11.0.16+8 (build 11.0.16+8)
OpenJDK 64-Bit Server VM Temurin-11.0.16+8 (build 11.0.16+8, mixed mode)
イメージにentrypointが設定されていてこの方法を実行できない場合は、次の例のように--entrypointパラメーターで上書きできます。
docker run --rm -it --entrypoint java tomcat:8 -version
opnjdk version "17.0.4" 2022-07-19
OpenJDK Runtime Environment Temurin-17.0.4+8 (build 17.0.4+8)
OpenJDK 64-Bit Server VM Temurin-17.0.4+8 (build 17.0.4+8, mixed mode, shareing)
DockerコンテナにJavaをインストールするにはどうすればよいですか?
コンテナイメージにJavaをインストールするには、必要なバージョンを含む公式のベースイメージを使用するのがおすすめです。例:
FROM eclipse-temurin:11.0.16_8-jdk
COPY myapp.jar /myapp.jar
...
DockerコンテナでJavaのバージョンを変更するにはどうすればよいですか?
コンテナイメージで使用するJavaのバージョンを変更するには、ベースイメージを希望するバージョンに更新するのが最適です。DockerfileのFROM行を必要なバージョンの正しいタグに変更し、適切なタグを付けてイメージを再ビルドしてから、イメージレジストリにプッシュします。最後に、デプロイで新しいタグを使用するように更新してください。
