新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第3回です。第2回までで Docker の基本操作を身につけました。今回は Dockerfile を書いて、本シリーズを通して育てる模擬アプリ fanclub-api の Backend イメージを自分でビルドします。Java(JDK 25 + Payara Micro)アプリを、マルチステージビルドで無駄のない実行イメージに仕上げ、JVM ヒープとコンテナのメモリ制限の関係(Container-aware JVM)まで押さえます。
動作確認バージョン:AlmaLinux 10.2 / Docker CE 29.6.0 / JDK 25(eclipse-temurin)/ Maven 3.9 / Payara Micro 7.2026.4(2026-06-22 時点)
今ここマップ(全 19 回中の現在地)
現在地は第 1 部「コンテナと Docker」の第 3 回です。ここで模擬アプリ fanclub-api の Backend が初めて登場します。
- 第 1 部 コンテナと Docker(第 1〜4 回)← 今ここ
- 第 2 部 Kubernetes 基礎(第 5〜6 回)
- 第 3 部 アプリリソース(第 7〜11 回)
- 第 4 部 ワークロード戦略(第 12〜14 回)
- 第 5 部 セキュリティ基礎(第 15〜16 回)
- 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)
この回のゴール
- Dockerfile を作成し、マルチステージビルドで Java + Payara Micro イメージをビルドできる。
- マルチステージビルドが「ビルド用の重い道具を実行イメージに残さない」仕組みだと説明できる。
- JVM ヒープとコンテナの
limits.memoryの関係(Container-aware JVM)を理解できる。
模擬アプリ fanclub-api と Backend のコード
fanclub-api は会員管理を行う 3 層構成のアプリです。Frontend(Nginx)・Backend(Java REST API)・Database(PostgreSQL)から成り、本シリーズを通じて段階的に組み上げます。今回はその中核となる Backend(JAX-RS による REST API + MicroProfile Health)をビルドします。
Backend の Maven プロジェクト構成は次のとおりです。
fanclub-api/
├── pom.xml
├── Dockerfile
├── settings.xml # Maven のプロキシ設定(ビルドステージで使用)
└── src/main/java/com/example/fanclub/
├── AppConfig.java # JAX-RS 有効化(@ApplicationPath("/api"))
├── Member.java # JPA エンティティ(members テーブル)
├── MembersResource.java # JAX-RS エンドポイント(CRUD)
├── MembersService.java # ビジネスロジック
├── DbConfig.java # EntityManagerFactory を環境変数から構築(CDI)
├── FanclubDb.java # CDI 修飾子(EntityManagerFactory の一意化)
└── HealthChecks.java # MicroProfile Health(live / ready / started)
pom.xml の要点です。Jakarta EE 11 と MicroProfile は実行時に Payara Micro が提供するため provided、PostgreSQL JDBC ドライバは WAR に同梱するため compile とします。パッケージングは war。web.xml を持たない構成のため、maven-war-plugin で failOnMissingWebXml を false にしてビルドを通します。
<packaging>war</packaging>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<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>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
</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>
REST エンドポイント(MembersResource.java)は会員の CRUD を担います。ベースパスは /api、リソースパスは /members です。抜粋を示します(PUT / DELETE / search も同じ形です)。
@Path("/members")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MembersResource {
@Inject
private MembersService service;
@GET
public Response list() {
return Response.ok(service.findAll()).build();
}
@POST
public Response create(Member input) {
Member created = service.create(input);
return Response.status(Response.Status.CREATED).entity(created).build();
}
@GET
@Path("/{id}")
public Response get(@PathParam("id") Long id) {
return service.findById(id)
.map(m -> Response.ok(m).build())
.orElseGet(() -> Response.status(404).build());
}
}
ヘルスチェック(HealthChecks.java)は MicroProfile Health で 3 種類(liveness / readiness / startup)を公開します。@Liveness / @Readiness / @Startup を付けるだけで、Payara Micro が /health/live・/health/ready・/health/started と、それらを集約した /health のエンドポイントを自動公開します(ルーティングを書く必要はありません)。これは第12回で扱う Kubernetes の 3 つの Probe(liveness / readiness / startup)にそのまま対応します。Liveness は「プロセスが生きていれば常に UP」、Readiness は「DB に疎通できるときだけ UP」、Startup は「起動処理が完了したら UP」とするのが定石です。次に liveness と readiness の例を示します(startup も同じ書き方で、完全なソースに含まれます)。
@Liveness
@ApplicationScoped
class LivenessCheck implements HealthCheck {
public HealthCheckResponse call() {
return HealthCheckResponse.up("fanclub-api-live");
}
}
@Readiness
@ApplicationScoped
class ReadinessCheck implements HealthCheck {
@Inject private MembersService service;
public HealthCheckResponse call() {
boolean dbUp;
try { dbUp = service.isDatabaseReachable(); }
catch (RuntimeException e) { dbUp = false; }
return HealthCheckResponse.named("fanclub-api-ready").status(dbUp).build();
}
}
残りのファイル(Member.java のエンティティ、MembersService.java のビジネスロジック、DbConfig.java の EntityManagerFactory 構築)を含む完全なソースは、本シリーズの教材一式に同梱しています。本記事ではビルドに集中するため、ここからは Dockerfile に話を進めます。
Dockerfile の基本記法
Dockerfile はイメージの作り方を記述するテキストファイルです。主要な命令を押さえます。
FROM:ベースイメージを指定する(イメージの出発点)。WORKDIR:以降の作業ディレクトリを設定する。COPY/ADD:ファイルをイメージに取り込む(ADDは URL 取得や展開も可能)。RUN:ビルド時にコマンドを実行する(各RUNが 1 レイヤーになる)。ENV:環境変数を設定する。EXPOSE:コンテナが待ち受けるポートを宣言する(ドキュメント目的)。ENTRYPOINT/CMD:コンテナ起動時に実行するコマンドを指定する。
各命令はイメージの「レイヤー」を作り、Docker はレイヤー単位でビルド結果をキャッシュします。変更の少ないものを先に、変更の多いものを後に COPY すると、キャッシュが効いてビルドが速くなります。後述の Dockerfile で pom.xml を先にコピーして依存解決し、ソースを後からコピーしているのはこのためです。
マルチステージビルド
Java アプリのビルドには Maven と JDK が必要ですが、実行に必要なのは JRE と成果物の WAR だけです。ビルド用の道具を実行イメージに残すとサイズが膨らみ、攻撃対象も増えます。そこで マルチステージビルドを使い、「ビルド用ステージ」で WAR を作り、「実行用ステージ」にはその成果物だけをコピーします。

fanclub-api Backend の Dockerfile(第3回時点)です。
# ===== ビルドステージ =====
FROM maven:3.9-eclipse-temurin-25 AS build
COPY settings.xml /root/.m2/settings.xml
WORKDIR /app
COPY pom.xml .
RUN mvn -B --no-transfer-progress dependency:go-offline
COPY src ./src
RUN mvn -B --no-transfer-progress package
# ===== 実行ステージ =====
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", "java $JAVA_OPTS -jar payara-micro.jar --deploy app.war --port 8080 --contextRoot /"]
- ビルドステージは
maven:3.9-eclipse-temurin-25(Maven と JDK 25 を同梱)。COPY --from=buildで成果物だけを実行ステージへ渡します。 - 実行ステージは
eclipse-temurin:25-jre(JRE のみで軽量)。 - Payara Micro 本体は Maven Central から取得します(
nexus.payara.fishは 7.2026.x が HTTP 404 のため使用しません)。 --contextRoot /によりアプリがルートに配置され、/api/membersや/health/liveが有効になります。COPY --from=build /app/target/fanclub-api.warのfanclub-api.warという名前は、pom.xmlの<finalName>fanclub-api</finalName>から決まります(finalNameを変えると、このCOPYのパスも合わせて変える必要があります)。ENTRYPOINTを["sh", "-c", "java $JAVA_OPTS …"]とsh -cで包んでいるのは、環境変数$JAVA_OPTSを展開させるためです。["java", …]という exec 形式のままだと$JAVA_OPTSはシェルを通らず、変数が展開されません。
ビルドステージ先頭の COPY settings.xml /root/.m2/settings.xml は Maven のプロキシ設定です。本シリーズの検証環境はプロキシ経由で外部接続するため、Maven の依存取得にプロキシが要ります。ここが要注意点で、Maven は JVM の -Dhttp.proxyHost プロパティを既定では使わず、settings.xml の <proxies> を参照します。次の settings.xml をプロジェクト直下に置き、ビルドステージへコピーします(Maven Central は HTTPS のため protocol が https の proxy が要点)。
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
<proxies>
<proxy>
<id>corp-https</id>
<active>true</active>
<protocol>https</protocol>
<host>alma-proxy</host>
<port>3128</port>
<nonProxyHosts>localhost|127.0.0.1</nonProxyHosts>
</proxy>
<proxy>
<id>corp-http</id>
<active>true</active>
<protocol>http</protocol>
<host>alma-proxy</host>
<port>3128</port>
<nonProxyHosts>localhost|127.0.0.1</nonProxyHosts>
</proxy>
</proxies>
</settings>
<host> は自環境のプロキシのホスト名または IP に置き換えます(本シリーズでは第1回で用意した alma-proxy。名前解決できない環境では IP アドレスを直接指定します)。プロキシの無い環境では <proxies> を空にした <settings/> を置けば構いません。なお Payara 本体の ADD による取得とベースイメージの pull は、第1回で設定した Docker デーモンのプロキシ経由で行われます(Maven の settings.xml とは別系統です)。
補足:この Dockerfile は root で実行する最小構成です。非 root 実行や読み取り専用ルートファイルシステム(readOnlyRootFilesystem)への対応は、セキュリティを扱う第15回で追加し、その時点でイメージを v1.0.0 に更新します。本回のイメージタグは v0.1.0 です。
JDK 25 LTS と Container-aware JVM
本シリーズは 2026 年の新しい LTS である JDK 25 を採用します。JVM はかつてホストの物理メモリを基準にヒープを決めていたため、コンテナのメモリ制限を無視して確保し、limits.memory 超過で OOMKilled になる事故が起きていました。JDK 11 以降は Container-aware JVM(-XX:+UseContainerSupport が既定で有効)となり、コンテナの cgroup メモリ制限を自動検出してヒープを計算します。
Dockerfile の JAVA_OPTS="-XX:MaxRAMPercentage=75.0" は「制限値の 75% までをヒープ上限にする」設定です。たとえばコンテナの limits.memory が 512Mi なら、ヒープ上限は約 384Mi に収まります。残り 25% は Metaspace やスレッドスタックなどに使われます。この設定により、Kubernetes でメモリ制限を付けたときに OOMKilled を避けやすくなります(第7回で実際に OOMKilled を起こして確認します)。

ビルドと起動確認
イメージをビルドします。Dockerfile・settings.xml・pom.xml・src を置いた fanclub-api/ ディレクトリに移動し、そこで次のコマンドを実行します。末尾の . はビルドコンテキスト(=カレントディレクトリ)を指し、ここに含まれるファイルだけが COPY の対象になります。Maven のプロキシは前述の settings.xml で設定済みのため、ビルドコマンドはシンプルです。タグは fanclub-backend:0.1.0 とします。
実行コマンド:
$ docker build -t fanclub-backend:0.1.0 .
ビルド中、Maven 起動時に WARNING: ... restricted method in java.lang.System という警告が出ることがありますが、これは JDK 25 の Project Panama 関連の警告で動作には影響しません。
ビルドしたイメージを起動します。Backend は 8080 番で待ち受けます。
実行コマンド:
$ docker run -d --name backend -p 8080:8080 fanclub-backend:0.1.0
Payara Micro の起動には数秒かかります(実測でアプリのデプロイに約 2.6 秒、起動からヘルスチェックに応答できるまで合わせて 6〜7 秒前後)。起動後、Liveness のヘルスチェックを叩いて確認します。この時点では DB を用意していないため Readiness(/health/ready)は DOWN ですが、Liveness(/health/live)はプロセスが生きていれば UP を返します。
実行コマンド:
$ curl -s http://localhost:8080/health/live
実行結果(例):
{"status":"UP","checks":[{"name":"fanclub-api-live","status":"UP","data":{}}]}
"status":"UP" が返れば、マルチステージビルドで作った Backend イメージが正しく起動しています。これで模擬アプリ Backend の v0.1.0 がローカルに揃いました。
やってみよう
- fanclub-api の Backend ソース一式(pom.xml / src / Dockerfile / settings.xml)を
fanclub-api/ディレクトリに用意する。 - その
fanclub-api/へ移動しdocker build -t fanclub-backend:0.1.0 .でビルドする(プロキシ環境ではsettings.xmlの<proxies>を設定しておく)。 docker imagesでビルドしたイメージのサイズを確認する(実行ステージが JRE のみで軽量なことを確認)。docker run -d --name backend -p 8080:8080 fanclub-backend:0.1.0で起動し、curl http://localhost:8080/health/liveがUPを返すことを確認する。docker logs backendで Payara Micro の起動ログを確認する。
理解度チェック
次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。
- マルチステージビルドは、ビルド用の道具を実行イメージに残さないことでイメージを小さくできる。
COPY --from=buildは、別ステージの成果物を現在のステージに取り込む。- 変更の多いソースを先に、変更の少ない
pom.xmlを後にCOPYするとビルドキャッシュが効きやすい。 - JDK 11 以降は
-XX:+UseContainerSupportが既定で有効で、コンテナのメモリ制限を検出する。 -XX:MaxRAMPercentage=75.0はヒープ上限をメモリ制限の 75% に抑える設定である。- Payara Micro は
nexus.payara.fishから取得するのが本シリーズの正しい方法である。 - DB が無い状態でも Liveness(
/health/live)は UP を返す設計である。
解答
- 1. ○:実行ステージには JRE と WAR だけを置くため軽量・安全になる。
- 2. ○:
--from=<ステージ名>で別ステージの成果物をコピーする。 - 3. ×:逆。変更の少ない
pom.xmlを先にコピーして依存解決をキャッシュし、変更の多いソースを後にする。 - 4. ○:Container-aware JVM により cgroup のメモリ制限を基準にヒープを計算する。
- 5. ○:制限値の 75% をヒープ上限とし、残りを Metaspace 等に充てる。
- 6. ×:
nexus.payara.fishは 7.2026.x が 404。Maven Central から取得する。 - 7. ○:Liveness はプロセス生存のみを見る。DB 障害で再起動ループに陥らせない設計。
まとめ
本記事では、模擬アプリ fanclub-api の Backend を題材に、Dockerfile の基本記法とマルチステージビルドを学び、JDK 25 + Payara Micro の実行イメージ fanclub-backend:0.1.0 をビルドしました。Container-aware JVM とヒープ比率の設定は、後の Kubernetes でメモリ制限を付ける際の土台になります。MicroProfile Health の 3 種チェックは、第12回の 3 つの Probe に直結します。
次回予告
次回(第4回)は、ビルドしたイメージを保管するコンテナレジストリを構築します。fanclub-backend イメージを k8s-registry に push し、イメージタグ戦略(latest の罠・セマンティックバージョニング)と、Trivy による脆弱性スキャンを扱います。
シリーズ一覧
第1部:コンテナと Docker
- 第1回 コンテナ技術概念 + Docker 環境準備
- 第2回 Docker 基本操作
- 第3回 Dockerfile + マルチステージ + JDK 25 / Payara Micro イメージビルド ← 今ここ
- 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャン
第2部:Kubernetes 基礎
第3部:アプリリソース
- 第7回 Pod + Multi-container パターン
- 第8回 Service とネットワーキング
- 第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加
- 第10回 ConfigMap + Secret + ServiceAccount 基礎
- 第11回 Job + CronJob + DaemonSet
第4部:ワークロード戦略
- 第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践
- 第13回 Deployment 戦略補完(Blue/Green + Canary + Recreate)
- 第14回 ResourceQuota + LimitRange + Multi-tenant Namespace
