本記事には広告(アフィリエイトリンク)が含まれます。

Dockerfile+マルチステージビルド【CKAD第3回】

広告

動作確認バージョン: Docker CE 29.4.3 / containerd 2.2.3 / runc 1.3.5 / OpenJDK 25.0.3 LTS(Red_Hat ビルド)/ Apache Maven 3.9.9(Red Hat 3.9.9-3)/ Payara Micro 7.2026.4(build 7)/ AlmaLinux 10.1(kernel 6.12.0-124.55.3.el10_1)(2026-05-09 時点・k8s-ops 実機検証済・SP_vol1-pre-04 起点)

本回は Kubernetes 実践教科書 第1巻(CKAD 対応・全 19 回)の第3回です。第1部「コンテナと Docker」の全 4 回のうち、第3回は出来合いのイメージを使うのではなく 自分でコンテナイメージを作る ステップを扱います。

Dockerfile の主要命令(FROM / WORKDIR / COPY / ADD / RUN / ENV / EXPOSE / ENTRYPOINT / CMD)、レイヤー構造とビルドキャッシュ、マルチステージビルドの仕組みと教育的意義、そして fanclub-api Backend(v0.1.0)の初登場までを通して扱います。CKAD D1(Application Design and Build・配点 20 %)の中核「コンテナイメージの定義・ビルド・変更」に直結する回です。

第2回からの継承状態確認(SP_vol1-pre-03 状態・第2回完走スナップショットを起点として演習を開始):

  • k8s-ops(AlmaLinux 10.1・2 vCPU / 6 GB / 40 GB)に Docker CE 29.4.3 が導入済み
  • nginx:1.27-alpine / alpine:latest / hello-world:latest イメージが残存(第2回演習で pull 済み)
  • JDK 25 / Maven / Payara Micro は 未インストール(第3回でインストールする)

今ここマップ(第1巻 19 回中の現在位置):

第1部 コンテナとDocker
    第1回: コンテナ技術概念 + Docker環境準備  [完了]
    第2回: Docker基本操作  [完了]
  ★ 第3回: Dockerfile + マルチステージビルド + JDK 25/Payara Micro  ← 今ここ
    第4回: コンテナレジストリ + Trivy スキャン

第2部 Kubernetes基礎(第5〜6回)
第3部 アプリリソース(第7〜11回)
第4部 ワークロード戦略(第12〜14回)
第5部 セキュリティ基礎(第15〜16回)
第6部 パッケージ管理 + HTTPS公開(第17〜19回)

第3回を終えると、以下を習得した状態になります。

  • Dockerfile の主要命令(FROM / WORKDIR / COPY / ADD / RUN / ENV / EXPOSE / ENTRYPOINT / CMD)の役割を説明できる
  • レイヤー構造とビルドキャッシュの仕組みを説明できる
  • マルチステージビルドで Java アプリのイメージサイズを最小化できる
  • JDK 25 LTS の Container-aware JVM(-XX:+UseContainerSupport)の意味を説明できる(第7回 OOMKilled 演習への布石)
  • fanclub-api Backend v0.1.0 のイメージをビルドし、ヘルスエンドポイントで動作確認できる

模擬アプリ進捗(第3回終了時点)

第3回: [Backend(Java)] ← Dockerfile を書いた(v0.1.0)  ← 今回
第4回: [Backend → k8s-registry] ← レジストリに push する(v0.2.0)
広告

Dockerfile とは何か(記法全体像)

Dockerfile はコンテナイメージのレシピです。どのベースイメージから始め、どのファイルをコピーし、どのコマンドを実行するかを記述します。docker build コマンドが Dockerfile を上から順に読み、各命令を「レイヤー」として積み重ねてイメージを構築します。

主要命令一覧

命令用途記述例
FROMベースイメージを指定(Dockerfile の最初の命令)FROM eclipse-temurin:25-jre AS runtime
WORKDIR以降の命令の作業ディレクトリを設定(存在しない場合は自動作成)WORKDIR /opt/payara
COPYホスト / 前ステージからファイルをコピーCOPY pom.xml .
ADDCOPY の上位互換(URL ダウンロード・tar 展開が追加可能)ADD https://... payara-micro.jar
RUNイメージビルド時にコマンドを実行し、結果をレイヤーとして保存RUN mvn package -B --no-transfer-progress
ENV環境変数を設定(コンテナ実行時にも有効)ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
EXPOSEコンテナがリスンするポートを文書化(実際の公開は docker run -p が担当)EXPOSE 8080
ENTRYPOINTコンテナ起動時のメインプロセスを指定(exec form 推奨)ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar payara-micro.jar ..."]
CMDENTRYPOINT のデフォルト引数 / ENTRYPOINT がない場合の実行コマンドCMD ["--port","8080"]

ENTRYPOINT vs CMD の重要な違い(CKAD 頻出論点):

  • ENTRYPOINT: docker run 時の通常のコマンドライン引数で上書きできない(--entrypoint フラグでのみ強制上書き可能)
  • CMD: docker run <image> <args><args> で上書き可能
  • K8s との対応: command: フィールドが ENTRYPOINT を上書き / args: フィールドが CMD を上書きする

shell form と exec form

ENTRYPOINT ["java", "-jar", "app.jar"]      (exec form 推奨:Java プロセスが PID 1 になる → SIGTERM を受け取れる)

ENTRYPOINT java -jar app.jar                (shell form 非推奨:sh プロセスが PID 1 になる → SIGTERM が Java に届かない)

Payara Micro 用の Dockerfile(後述 H2-9)では $JAVA_OPTS の環境変数展開のために ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar ..."] 形式を採用します。配列の中で sh -c を経由しつつ、コマンド先頭に exec を付けることで、sh プロセスを java プロセスに置換します。

これにより Java プロセスが PID 1 となり、docker stop が送る SIGTERM が直接 Java に届くため Graceful Shutdown を実現できます。exec を付け忘れると sh が PID 1 のまま残り、SIGTERM が sh で消費されて Java へ届かないため、コンテナ停止時に強制終了(SIGKILL)される問題が生じます。

レイヤー構造とビルドキャッシュの仕組み

RUN / COPY / ADD 命令はイメージの「レイヤー」を1つ追加します。レイヤーはコンテンツのハッシュ値で識別され、同一ハッシュのレイヤーは再利用(キャッシュ)されます。

ビルドキャッシュの基本ルール

  • 前のステップが変わっていなければ、そのレイヤーはキャッシュから再利用される
  • 特定のレイヤーが変わると、それ以降のレイヤーはすべてキャッシュ無効化される
  • 頻繁に変わる COPY src は後ろに配置し、あまり変わらない RUN mvn dependency:go-offline を前に置くのが定番パターン
  • ADD <URL> はキャッシュが効かない:外部 URL からのフェッチ命令はビルドのたびに必ずダウンロードが発生する(CKAD 出題範囲・H2-9 の Dockerfile で ADD https://... を使う際に再確認)

レイヤーキャッシュ最適化の発展パターン(原理理解用の参考例):

FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -B --no-transfer-progress

このパターンでは src/ の変更時に依存関係ダウンロードキャッシュが有効です。pom.xml 変更時のみ依存関係を再取得します。CI/CD 環境でビルド時間を短縮する典型パターンとして本番運用で広く採用されています。

ただし本記事の演習(H2-9)で最終的に作成する Dockerfile は app-spec.md §12 のシンプル構成で、mvn dependency:go-offline を使わない最小構成です。「まず動くものを作る」→「レイヤーキャッシュ最適化は発展知識」という順序で本シリーズは設計しています。上記パターンは原理理解用の参考例として捉えてください。

Dockerfile レイヤー構造とビルドキャッシュの仕組み図 - FROM/WORKDIR/COPY pom.xml/RUN mvn dependency:go-offline までの 4 命令は pom.xml が変わらない限りキャッシュ再利用される。COPY src/RUN mvn package の 2 命令は src 変更でキャッシュが無効化される境界を示す。下に行くほどビルドキャッシュ最大活用の設計原則を可視化。

マルチステージビルドの仕組みと教育的意義

第3回の最大の技術的核心です。Java アプリをコンテナ化する際の典型的な問題から始めます。Maven + JDK でビルドすると、最終イメージに Maven・JDK(数百 MB)が含まれてしまいます。マルチステージビルドはこの問題を解決します。

マルチステージビルドの構造

FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn package -B --no-transfer-progress

FROM eclipse-temurin:25-jre AS runtime
WORKDIR /opt/payara
ADD https://repo.maven.apache.org/maven2/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar payara-micro.jar
COPY --from=build /app/target/fanclub-api.war app.war
EXPOSE 8080
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar payara-micro.jar --deploy app.war --port 8080 --contextRoot /"]
マルチステージビルドの流れ図 - 左ビルドステージ(maven:3.9-eclipse-temurin-25)で Maven 3.9 + JDK 25 + src + fanclub-api.war をビルド → COPY --from=build 矢印で WAR のみを移動 → 右実行ステージ(eclipse-temurin:25-jre)で JRE 25 + payara-micro.jar + コピーされた WAR を保持。Maven・JDK は最終イメージに含まれない。

マルチステージビルドの効果

  • ビルドステージ(maven:3.9-eclipse-temurin-25): Maven + JDK 25 + 依存関係 → 数 GB 規模
  • 実行ステージ(eclipse-temurin:25-jre): JRE のみ + Payara Micro JAR + WAR → 後述 H2-9 で実測 657 MB(DISK USAGE)/ 206 MB(CONTENT SIZE)
  • COPY --from=build で WAR だけを取り出す。Maven・ソースコード・JDK は最終イメージに含まれない

COPY --from=<stage> の文法

  • --from=build: AS build と名付けたステージから取得
  • --from=0: ステージ番号(0 始まり)での指定も可能
  • --from=<image>: 別イメージからの取得も可能(応用)

第15回 SecurityContext + Pod Security Standards で軽量化(distroless 等)を扱う段階で、現状の 657 MB(DISK USAGE)からさらに削減する技法を学びます。第3回時点では「ビルド成果物だけを切り出して別イメージに移す」という概念を確実に身につけることを目標とします。

JDK 25 LTS の選定理由 + Container-aware JVM

本シリーズが JDK 25 LTS を採用する理由を整理します。

観点JDK 25 の位置づけ
LTS(長期サポート)Java SE 8 / 11 / 17 / 21 / 25 が LTS。JDK 25 は 2025 年 9 月 GA・2033 年以降までサポート
業界標準2026 年時点の最新 LTS として Java コミュニティが採用を推奨
Jakarta EE 11 対応Payara Micro 7.x が Jakarta EE 11(JDK 21+)対応・JDK 25 で動作確認済
Container-aware JVMJDK 11 以降でデフォルト ON(後述)

Container-aware JVM(-XX:+UseContainerSupport

JDK 11 以降、JVM はデフォルトでコンテナの cgroup 設定を認識します(OpenJDK バグトラッカー JDK-8146115 で導入)。コンテナの cgroup メモリ制限を自動検出し、ホスト物理メモリではなく制限値を基準にヒープを計算します。

旧来の問題(JDK 8 以前):
  JVM がホスト物理メモリ 64 GB を検出 → ヒープを 16 GB に設定
  コンテナの limits.memory: 512Mi を無視 → 起動直後に OOMKilled

JDK 11 以降の動作(UseContainerSupport = true がデフォルト):
  JVM がコンテナの cgroup メモリ制限 512 Mi を検出
  ヒープを 512 Mi の 25 % 〜最大約 128 Mi に自動設定(デフォルト比率)
  または MaxRAMPercentage を指定して明示的に比率を決める

JDK 25 のデフォルト RAM 比率:明示指定がない場合の JDK 25 デフォルトは以下の通りです。CKAD 試験では具体値の暗記は不要ですが、第7回 OOMKilled 演習に向けた定量的理解として把握しておきます。

  • -XX:InitialRAMPercentage=1.5625(初期ヒープ約 1.5 %)
  • -XX:MaxRAMPercentage=25.0(最大ヒープ 25 %)
  • -XX:MinRAMPercentage=50.0(小容量コンテナ用最小ヒープ 50 %)

第3回の Dockerfile では ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0" を記述します。K8s の resources.limits.memory との整合(OOMKilled の防止設計)は第7回で本格的に扱います。第3回では「Docker 単体でも JVM がコンテナのメモリ制限を認識する」という概念的な理解にとどめます。

Payara Micro 7.2026.4 の入手方法と仕組み

Payara Micro は WAR ファイルを実行可能 JAR として動作させるサーブレットコンテナです。Spring Boot の executable jar に近い概念で、java -jar payara-micro.jar --deploy app.war の 1 コマンドでアプリを起動できます。

入手元の注意点(後述 H2-10 の現場ヒヤリハット事例 1 として詳述):

Payara 公式の nexus.payara.fish では 7.2026.x の jar ファイルが HTTP 404 を返します。正しい入手元は Maven Central です。

https://repo.maven.apache.org/maven2/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar

Dockerfile では ADD 命令で URL を直接指定します。

ADD https://repo.maven.apache.org/maven2/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar payara-micro.jar

ADD <URL> を使うと Docker がビルド時に直接ダウンロードします。alma-proxy 経由の接続環境では repo.maven.apache.org が whitelist 登録済み(環境構築時に確認済み)であれば追加設定は不要です。

Payara Micro の起動オプション

java -jar payara-micro.jar \
  --deploy app.war \
  --port 8080 \
  --contextRoot /
オプション説明
--deploy <war>デプロイする WAR ファイルを指定
--port <port>HTTP リスンポートを指定(デフォルト: 8080)
--contextRoot <path>コンテキストルートを指定。/ を指定すると JAX-RS アプリの @ApplicationPath が直接有効

MicroProfile Health の起動確認

Payara Micro は MicroProfile Health を組み込みで提供します。起動後に以下で疎通確認します。

GET http://localhost:8080/health/live  →  {"status":"UP","checks":[...]}

ヘルスエンドポイントのパスについて重要な注意点:MicroProfile Health は JAX-RS の @ApplicationPath(本シリーズでは /api)の影響を受けず、コンテキストルート直下に独立してバインドされます。

したがって --contextRoot / の場合、ヘルスエンドポイントは /health/live(ルート直下・/api プレフィックスなし)が正しいパスです。本記事の H2-9 実機検証で --contextRoot / 設定下でも /health/live がルート直下にバインドされることを確認しています。

fanclub-api Backend のサンプルコード

第3回では fanclub-api の最小構成(v0.1.0)を作成します。CRUD は第9回以降で完成させるため、今回は Health エンドポイントのみ実装します。

ファイル構成

fanclub-api/
├── pom.xml
├── Dockerfile
└── src/main/java/com/example/fanclub/
    ├── HealthChecks.java    (MicroProfile Health の Liveness 実装)
    └── AppConfig.java       (JAX-RS アプリケーション設定)

pom.xml(完全版)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>fanclub-api</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>11.0.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile</groupId>
            <artifactId>microprofile</artifactId>
            <version>6.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>fanclub-api</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

AppConfig.java

package com.example.fanclub;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")
public class AppConfig extends Application {
}

HealthChecks.java

package com.example.fanclub;

import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;

@ApplicationScoped
@Liveness
public class HealthChecks implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.up("fanclub-api-live");
    }
}

設計上の注記:本シリーズでは @Liveness / @Readiness / @Startup を 1 クラス 1 アノテーションで別クラスに分離する設計を採用します(v0.1.0 では Liveness のみのため HealthChecks.java 1 クラス)。

第7回 Pod + Multi-container パターンで Readiness / Startup を追加する際は別クラス(ReadinessCheck.java / StartupCheck.java)として分離します。同一クラスに 3 アノテーションを付与する設計も MicroProfile 仕様上は可能ですが、責務分離の観点から本シリーズは別クラス分離を採用しています。

やってみよう ① JDK 25 + Maven 環境準備と Maven プロジェクト作成

前提状態:SP_vol1-pre-03(Docker CE 29.4.3 導入済み・JDK 25 / Maven は未インストール)

ステップ 1:JDK 25 LTS をインストール

実行コマンド:

$ sudo dnf install -y java-25-openjdk-devel

実行結果(末尾抜粋):

完了しました!

ステップ 2:JDK 25 のバージョン確認

実行コマンド:

$ java -version

実行結果:

openjdk version "25.0.3" 2026-04-21 LTS
OpenJDK Runtime Environment (Red_Hat-25.0.3.0.9-1) (build 25.0.3+9-LTS)
OpenJDK 64-Bit Server VM (Red_Hat-25.0.3.0.9-1) (build 25.0.3+9-LTS, mixed mode, sharing)

ステップ 3:alternatives で java リンク先を確認

AlmaLinux 10.1 の標準 JDK パッケージは alternatives で java コマンドのリンク先を一元管理します。インストール後の状態を確認します。

実行コマンド:

$ alternatives --display java

実行結果:

java - status is auto.
 link currently points to /usr/lib/jvm/java-25-openjdk/bin/java
/usr/lib/jvm/java-25-openjdk/bin/java - priority 1
 follower jre: /usr/lib/jvm/java-25-openjdk
 follower alt-java: /usr/lib/jvm/java-25-openjdk/bin/alt-java
 follower jcmd: /usr/lib/jvm/java-25-openjdk/bin/jcmd
 follower keytool: /usr/lib/jvm/java-25-openjdk/bin/keytool
 follower rmiregistry: /usr/lib/jvm/java-25-openjdk/bin/rmiregistry
Current `best' version is /usr/lib/jvm/java-25-openjdk/bin/java.

link currently points to /usr/lib/jvm/java-25-openjdk/bin/java から JDK 25 が java として有効になっていることが確認できます。複数 JDK が共存する環境で切り替える必要がある場合は sudo alternatives --config java で対話的に選択できます。

ステップ 4:Maven をインストール

実行コマンド:

$ sudo dnf install -y maven

実行結果(末尾抜粋):

  maven-openjdk21-1:3.9.9-3.el10_1.0.1.noarch
  maven-resolver-1:1.9.18-4.el10.noarch
  maven-shared-utils-3.4.2-8.el10.noarch
  maven-wagon-3.5.3-9.el10.noarch
  plexus-cipher-2.0-12.el10.noarch
  plexus-classworlds-2.8.0-4.el10.noarch
  plexus-containers-component-annotations-2.2.0-4.el10.noarch
  plexus-interpolation-1.27-4.el10.noarch
  plexus-sec-dispatcher-2.0-14.el10.noarch
  plexus-utils-3.5.1-8.el10.noarch
  publicsuffix-list-20240107-5.el10.noarch
  sisu-1:0.3.5-14.el10.noarch
  slf4j-1.7.32-13.el10.noarch

完了しました!

依存パッケージとして maven-openjdk21 がインストールされている点に注目してください。これが後述する「ホスト側 mvn package 失敗」の伏線になります。

ステップ 5:Maven のバージョン確認

実行コマンド:

$ mvn -version

実行結果:

Apache Maven 3.9.9 (Red Hat 3.9.9-3)
Maven home: /usr/share/maven
Java version: 21.0.11, vendor: Red Hat, Inc., runtime: /usr/lib/jvm/java-21-openjdk
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.12.0-124.55.3.el10_1.x86_64", arch: "amd64", family: "unix"

ここで決定的な事実が判明します。java -version では JDK 25.0.3 が表示されたのに、mvn -version では Java version: 21.0.11runtime: /usr/lib/jvm/java-21-openjdk と出力されています。Maven 自体は OpenJDK 21 で動作するのです。

ステップ 6:Maven プロジェクトを作成(pom.xml + src 配置)

実行コマンド:

$ mkdir -p ~/fanclub-api/src/main/java/com/example/fanclub
$ cd ~/fanclub-api

続いて、本記事 H2-7「fanclub-api Backend のサンプルコード」で示した pom.xml / AppConfig.java / HealthChecks.java をそれぞれ配置します。第1回・第2回で利用した vi エディタで作成するか、もしくは cat > <ファイルパス> <<'EOF' 形式のヒアドキュメントで作成します(Java ソースは src/main/java/com/example/fanclub/ 配下)。

ステップ 7:演習① 完了(ホスト側 mvn package は実行しない)

コラム:ホストの JDK / Maven 整合性問題と、マルチステージビルドが解決する論点

多くの企業環境では「ホスト OS に最新 JDK と整合する Maven をインストールするのは管理コストが高い」という現実があります。AlmaLinux 10.1 の標準パッケージ管理では、dnf install maven をするだけで maven-openjdk21 が依存として入ってしまい、Maven 自体が OpenJDK 21 で動作します(ステップ 5 の mvn -version 出力で確認済み)。

このとき pom.xml で <maven.compiler.source>25</maven.compiler.source> を指定して mvn package を実行すると、javac 21 は target 25 を生成できないため BUILD FAILURE になります(実機で エラー: 25は無効なターゲット・リリースです を確認済み)。

この問題を解決するのが マルチステージビルド です。次の H2-9(演習②)では maven:3.9-eclipse-temurin-25 という公式 Docker イメージ(Maven + JDK 25 同梱)を builder ステージとして使うことで、ホスト OS の状態に左右されない再現可能なビルド環境を実現します。「ホスト OS に JDK 25 + Maven 3.9 を整合性を保ちながらインストールするのが面倒」という現実を、コンテナビルドが完全に解決します。これが第3回の主題「マルチステージビルドの教育的意義」の本質です。

演習① の完了状態

  • JDK 25.0.3 LTS が /usr/lib/jvm/java-25-openjdk にインストール済
  • alternatives で java リンクが JDK 25 を指す
  • Apache Maven 3.9.9(Red Hat 3.9.9-3)がインストール済(ただし Java 21 で動作)
  • ~/fanclub-api/ 配下に pom.xml と Java ソース(AppConfig.java / HealthChecks.java)が配置済
  • target/ ディレクトリは未生成(WAR は H2-9 のマルチステージビルド内で初めて生成される)

やってみよう ② Dockerfile 作成 → docker build → docker run → ヘルス確認

前提状態:H2-8 完了後(~/fanclub-api/pom.xml / src/ が存在・JDK 25 + Maven インストール済・target/ ディレクトリは空またはなし・WAR ファイルは本演習②のマルチステージビルド内で初めて生成される)

ステップ 1:Dockerfile を作成

~/fanclub-api/Dockerfile を以下の内容で作成します(app-spec.md §12 完成形)。

FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn package -B --no-transfer-progress

FROM eclipse-temurin:25-jre AS runtime
WORKDIR /opt/payara
ADD https://repo.maven.apache.org/maven2/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar payara-micro.jar
COPY --from=build /app/target/fanclub-api.war app.war
EXPOSE 8080
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar payara-micro.jar --deploy app.war --port 8080 --contextRoot /"]

注記(root ユーザー実行について):上記 Dockerfile には USER 命令がないため、コンテナはデフォルトで root として実行されます。本番運用では USER nobody または専用の non-root ユーザーで動かすのがベストプラクティスです。本シリーズでは段階的学習のため第3回時点では root 実行のままとし、第15回 RBAC + SecurityContext + Admission Controller 概念 + CRD 利用で non-root に変更する流れで扱います(第3巻 CKS 編でも CIS ベンチマーク観点で取り上げます)。

注記(ADD <URL> のビルドキャッシュ)ADD https://... は外部 URL をフェッチするため、Docker のビルドキャッシュが効かず毎回ダウンロードが発生します。本番でビルド頻度が高い場合は、Payara Micro JAR をプリフェッチして COPY で配置するか、社内 nexus / Artifactory にミラーリングする方法を採用します。

BuildKit (Docker 18.09+ デフォルト) では RUN --mount=type=cache を使ってダウンロード結果をキャッシュ層に保持する手法もあります(実務ではよく使われますが、本記事では教育目的でシンプルな ADD を採用)。レジストリ運用は第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャンで扱います。

ステップ 2:docker build でイメージをビルド

タグには 0.1.0 を明示的に指定します。本シリーズでは latest タグは使用しません(理由は 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャンで詳述・latest の罠とセマンティックバージョニング)。

実行コマンド:

$ docker build -t fanclub-backend:0.1.0 .

実行結果(末尾抜粋):

#14 [build 4/4] RUN mvn package -B --no-transfer-progress
#14 9.239 [INFO]
#14 9.240 [INFO] --- war:3.4.0:war (default-war) @ fanclub-api ---
#14 12.96 [INFO] Packaging webapp
#14 12.96 [INFO] Assembling webapp [fanclub-api] in [/app/target/fanclub-api]
#14 12.96 [INFO] Processing war project
#14 12.99 [INFO] Building war: /app/target/fanclub-api.war
#14 13.01 [INFO] ------------------------------------------------------------------------
#14 13.01 [INFO] BUILD SUCCESS
#14 13.01 [INFO] ------------------------------------------------------------------------
#14 13.01 [INFO] Total time:  12.033 s
#14 13.01 [INFO] Finished at: 2026-05-09T11:39:19Z
#14 13.01 [INFO] ------------------------------------------------------------------------
#14 DONE 13.1s

#15 [runtime 4/4] COPY --from=build /app/target/fanclub-api.war app.war
#15 DONE 0.1s

#16 exporting to image
#16 exporting layers 2.2s done
#16 exporting manifest sha256:22490cc5afd727f350b050e4211aae22c7255467555345dd7379e839050d33be 0.0s done
#16 exporting config sha256:bb00cc9c36c0007d27f32937354b32b4195ae6b67d9e50ce56e32d312ed04f9f 0.0s done
#16 exporting attestation manifest sha256:81fdaf7de8180bae2cbdf4e080a5342be2edd52b8505c1a5c1c207dd46f86529 0.0s done
#16 exporting manifest list sha256:3bdcfa296cf6b545aa2878f46b7c41bc5b65b3e05c497f19ae3cec13eb4b24ce 0.0s done
#16 naming to docker.io/library/fanclub-backend:0.1.0
#16 unpacking to docker.io/library/fanclub-backend:0.1.0 0.3s done
#16 DONE 2.6s

ビルドステージで BUILD SUCCESS(Maven の WAR パッケージング所要時間 12.033 秒)が表示され、続けて runtime ステージで COPY --from=build により WAR ファイルだけが取り出されます。最終的に docker.io/library/fanclub-backend:0.1.0 としてイメージが生成されます。

ステップ 3:イメージの確認

実行コマンド:

$ docker images fanclub-backend

実行結果(Docker 29.x 新フォーマット):

WARNING: This output is designed for human readability. For machine-readable output, please use --format.
IMAGE                   ID             DISK USAGE   CONTENT SIZE   EXTRA
fanclub-backend:0.1.0   3bdcfa296cf6        657MB          206MB

DISK USAGE 657 MB / CONTENT SIZE 206 MB です。CONTENT SIZE が 200 MB 超なのは Payara Micro JAR(約 80 MB)+ JDK 25 JRE ベースイメージ約 120 MB の合計です。第15回 SecurityContext + 軽量化の段階で distroless / alpine ベースへの移行を扱う際、この 657 MB を削減対象として再登場させます。

注記(Docker 29.x 出力フォーマット):上記 docker imagesWARNING: This output is designed for human readability... および列名 IMAGE / ID / DISK USAGE / CONTENT SIZE / EXTRA は Docker 29.x で導入された新フォーマットです。旧形式(REPOSITORY / TAG / IMAGE ID / CREATED / SIZE)の出力例を期待していると見え方が変わるため留意してください。第2回でも同様の新フォーマットを扱っています。

ステップ 4:コンテナを起動

実行コマンド:

$ docker run -d --name fanclub-backend-test -p 8080:8080 fanclub-backend:0.1.0

-p 8080:8080 の左側 8080ホストポート(k8s-ops 上で公開するポート)、右側 8080コンテナポート(コンテナ内で Payara Micro がリスンしているポート・EXPOSE 8080 と一致)です。CKAD 試験で `-p ホスト:コンテナ` の順番は頻出するため、ここで覚えておきます。

実行結果(コンテナ ID が返る):

d18d4d0e2a1145727a2a169f426b720ea8b7728913c91aeddcde840287c78545

ステップ 5:起動ログを確認(Payara Micro の起動時間を観察)

実行コマンド:

$ docker logs fanclub-backend-test

実行結果(末尾抜粋・JSON サマリー部分):

        "Host": "d18d4d0e2a11",
        "Http Port(s)": "8080",
        "Https Port(s)": "",
        "Instance Name": "Encouraging-Haddock",
        "Instance Group": "MicroShoal",
        "Hazelcast Member UUID": "f7166542-9e91-41e4-b0f4-01d1cdcc6836",
        "Deployed": [
            {
                "Name": "app",
                "Type": "war",
                "Context Root": "/"
            }
        ]
    }
}]]

Payara Micro URLs:
"http://d18d4d0e2a11:8080/"

'app' REST Endpoints:
GET	/api/application.wadl
GET	/openapi/
GET	/openapi/application.wadl


Payara Micro 7.2026.4 (build 7) ready in 6,676 (ms)

起動ログで読み取るべきポイントは複数あります。

  • Instance Name: Encouraging-Haddock は Payara Micro が起動毎にランダムに割り当てる動物名(環境ごとに動物名が変わるため、読者の手元では別の名前が表示されます)
  • Context Root: / は Dockerfile の --contextRoot / 指定がそのまま反映された結果
  • 'app' REST Endpoints/api/application.wadl が表示される = @ApplicationPath("/api") で JAX-RS ベースが /api 配下にバインドされたことの証跡
  • 起動時間 6,676 ms(約 6.7 秒)が 第7回 Pod + Multi-container パターンstartupProbe 設計(failureThreshold × periodSeconds = 最大 300 秒待機)の根拠になります

ステップ 6:ヘルスエンドポイントで動作確認

MicroProfile Health は @ApplicationPath/api)の影響を受けず、コンテキストルート直下にバインドされるため /health/live でアクセスします(H2-6 で詳述した仕様)。

実行コマンド(HTTP ステータスのみ確認):

$ curl -s -o /dev/null -w "HTTP=%{http_code}\n" http://localhost:8080/health/live

実行結果:

HTTP=200

実行コマンド(JSON レスポンス本文を確認):

$ curl -s http://localhost:8080/health/live

実行結果:

{"status":"UP","checks":[{"name":"fanclub-api-live","status":"UP","data":{}}]}

HealthChecks.java で実装した Liveness が fanclub-api-live という名前で UP を返していることが確認できます。第3回の到達物として fanclub-api Backend v0.1.0 が動作可能なコンテナイメージとして完成しました。

ステップ 7:後片付け

実行コマンド:

$ docker stop fanclub-backend-test
$ docker rm fanclub-backend-test

実行結果:

fanclub-backend-test
fanclub-backend-test

イメージ fanclub-backend:0.1.0 はそのまま残し、第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャンでレジストリへの push 演習に引き継ぎます。

現場ヒヤリハット

事例1:nexus.payara.fish で 404 が返ってきてビルドが失敗した

入社半年の D さんは、先輩から渡された古い Dockerfile を使って Payara Micro イメージをビルドしようとしました。Dockerfile には ADD https://nexus.payara.fish/repository/payara-artifacts/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar payara-micro.jar と書かれていました。

docker build を実行すると ADD の段階でエラーが出てビルドが止まりました。curl -I で URL を確認したところ HTTP 404 レスポンスが返ってきます。Payara 公式サイトの URL なのになぜ 404 になるのか、しばらく原因がわからずに 1 時間ほど悩みました。

原因:Payara 公式の nexus.payara.fish は 7.2026.x 系の jar を提供しておらず HTTP 404 を返します。Payara 社のリリース運用が変更され、Payara Micro JAR の正規配布元が Maven Central に移行していました。

対処法:Maven Central が正しい入手元です。URL を以下に差し替えます。

https://repo.maven.apache.org/maven2/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar

「公式ドキュメントに書かれていた URL」と「実際にアクセス可能な URL」が乖離するケースは現場で頻繁に発生します。Dockerfile に外部 URL を書く際は、ビルド前に手元の curl / wget で必ず疎通確認する習慣をつけることが事故防止の基本です。

事例2:OOMKilled — Payara Micro がコンテナ起動直後にクラッシュした

入社 1 年目の E さんは、Java アプリのコンテナを軽量化したいと考え、docker run-m 128m(メモリ制限 128 MB)を付けて Payara Micro コンテナを起動しました。docker run 自体は成功して exit ステータスは 0 でしたが、docker ps -a で確認すると STATUS が Exited (137) になっていました。docker logs を見ると Payara Micro がある程度起動したところで突然ログが途切れていました。

原因:JVM のヒープ + Metaspace + Native スレッドスタックの合計が 128 MB を超えて OOMKilled(Linux カーネルの OOM Killer による強制終了・終了コード 137)になりました。-XX:+UseContainerSupport がデフォルト ON のため、JVM は cgroup 制限 128 MB を検出します。

-XX:MaxRAMPercentage 未指定時の JDK 25 デフォルト比率はおおむね 25 % のため、ヒープ上限は約 32 MB に絞られますが、Payara Micro の起動には Metaspace(クラスメタデータ)+ Native スレッドスタック + Direct Memory も必要で、JVM 全体の要求メモリが 128 MB をあっさり超えてしまいました。

MaxRAMPercentage=75.0 を明示すればヒープ上限は 96 MB(128 × 0.75)になりますが、それでも Metaspace + Native 32 MB に収まらないケースが多いため、Payara Micro は最低 256 MB の制限が現実的です。

対処法

  • docker stats fanclub-backend-test でメモリ使用量を観察する(リアルタイムで RSS / cgroup 使用率を確認)
  • ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0" を Dockerfile で設定することで、JVM がコンテナのメモリ制限の 75 % をヒープ上限として自動設定する
  • Payara Micro の最小動作メモリは 256 MB 以上を推奨(k8s-ops の 6 GB RAM では制限なしで問題なし)

K8s の resources.limits.memory との整合(OOMKilled の意図的な発生と kubectl describe pod でのデバッグ)は第7回 Pod + Multi-container パターンで本格的に扱います。

今回のまとめ + 理解度チェック

第3回では以下を実施しました。

  • Dockerfile の主要命令(FROM / WORKDIR / COPY / ADD / RUN / ENV / EXPOSE / ENTRYPOINT / CMD)の役割を確認した
  • レイヤー構造とビルドキャッシュの仕組み・キャッシュ最適化パターンを理解した
  • マルチステージビルドで Maven ビルドステージと JRE 実行ステージを分離し、最終イメージを 657 MB(DISK USAGE)/ 206 MB(CONTENT SIZE)に収める方法を習得した
  • JDK 25 LTS と Container-aware JVM の概念(-XX:+UseContainerSupport がデフォルト ON)を確認した
  • Payara Micro 7.2026.4 の正しい入手元(Maven Central・nexus.payara.fish は HTTP 404)を確認した
  • AlmaLinux 10.1 の maven パッケージは maven-openjdk21 依存で動作するため、ホスト側 mvn package での <source>25</source> コンパイルは BUILD FAILURE となり、マルチステージビルドが解決手段になることを理解した
  • fanclub-api Backend v0.1.0 のイメージをビルドし、Payara Micro 起動時間 6,676 ms を観察し、/health/live ヘルスエンドポイントで {"status":"UP"} を確認した

fanclub-api の段階的な構築の流れ

第3回: [Backend(Java)] ← Dockerfile を書いた(v0.1.0)  ← 今ここ
第4回: [Backend → k8s-registry] ← レジストリに push する(v0.2.0)

理解度チェック(○× 形式・全 9 問)

問題文解答
1Dockerfile の ENTRYPOINTdocker run 時の通常のコマンドライン引数で上書きできる×
2マルチステージビルドでは、最終イメージには最後の FROM ステージの内容のみが含まれる
3COPY --from=build は、AS build と名付けたステージからファイルを取得する命令である
4JDK 25 の -XX:+UseContainerSupport はデフォルトで無効であり、明示的に ON にする必要がある×
5Dockerfile の EXPOSE 8080 を書くと、docker run 時に自動的にホストの 8080 ポートが公開される×
6Payara Micro 7.2026.4 は nexus.payara.fish から正しく取得できる×
7RUN mvn package の結果はレイヤーとしてキャッシュされ、pom.xmlsrc に変更がなければ次回ビルドで再利用される
8Kubernetes の Pod に command: フィールドを設定すると、Dockerfile の ENTRYPOINT が上書きされる
9Kubernetes の Pod に args: フィールドを設定すると、Dockerfile の ENTRYPOINT が上書きされる×

解説(重要問のみ)

  • 問1:ENTRYPOINTdocker run の通常の引数では上書きできません。強制上書きするには --entrypoint フラグが必要です。CMD は通常の引数で上書きできます。
  • 問4:JDK 11 以降、-XX:+UseContainerSupport はデフォルトで ON です。明示的に有効化する必要はありません。
  • 問5:EXPOSE はドキュメント的な宣言であり、実際のポート公開は docker run -p 8080:8080 で行います。
  • 問8・問9:K8s の command: は Dockerfile の ENTRYPOINT を、args:CMD を上書きします(args:ENTRYPOINT を上書きしない)。CKAD 試験で頻出の対応関係であり、本シリーズでも第7回 Pod 仕様で重要論点として再登場します。

次回予告第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャンでは、k8s-registry(Docker Registry)を構築し、第3回で作成した fanclub-backend:0.1.0 イメージをタグ付けして push します。latest タグの罠とセマンティックバージョニングの実務、Trivy v0.70.0 でのイメージ脆弱性スキャンによるセキュアなイメージ管理の基礎を扱います。第19回 fanclub-api 全機構連携の実技総ざらい総合演習に向けた基盤づくりが本格化します。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク