Series MapLesson 03 / 60
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Start HereOrdered learning track

Dockerfile for Java/JAX-RS Services

Praktik Dockerfile untuk Java 17+ JAX-RS service: base image, multi-stage build, dependency layering, non-root runtime, entrypoint, JVM options, dan production review checklist.

20 min read3951 words
PrevNext
Lesson 0360 lesson track01–11 Start Here
#dockerfile#java#jax-rs#maven+3 more

Part 003 — Dockerfile for Java/JAX-RS Services

Fokus part ini: bagaimana menulis dan mereview Dockerfile untuk service Java 17+ / JAX-RS / Jakarta RESTful Web Services yang akan berjalan sebagai container production di Kubernetes, EKS, AKS, on-prem, atau hybrid deployment.

Dockerfile untuk backend enterprise bukan sekadar:

FROM openjdk:17
COPY app.jar app.jar
CMD ["java", "-jar", "app.jar"]

Itu memang bisa jalan, tetapi belum tentu:

  • aman;
  • kecil;
  • reproducible;
  • cepat dibangun;
  • mudah dipromosikan antar environment;
  • berjalan sebagai non-root;
  • cocok dengan Kubernetes probes;
  • cocok dengan JVM container memory/CPU behavior;
  • mendukung graceful shutdown;
  • mudah di-debug saat CrashLoopBackOff, ImagePullBackOff, atau OOMKilled.

Untuk senior backend engineer, Dockerfile adalah deployment contract antara source code, build pipeline, image registry, runtime container, orchestrator, dan platform security policy.


1. Mental Model: Dockerfile sebagai Build Recipe dan Runtime Contract

Dockerfile punya dua sisi:

  1. Build-time contract

    • dari source code menjadi artifact;
    • dependency resolve;
    • Maven/Gradle build;
    • test/lint/package;
    • layer caching;
    • reproducibility;
    • vulnerability exposure dari base image dan dependencies.
  2. Runtime contract

    • process apa yang dijalankan;
    • user apa yang menjalankan process;
    • working directory;
    • environment variable;
    • port yang diharapkan;
    • filesystem yang dibutuhkan;
    • signal handling;
    • JVM options;
    • logging behavior;
    • health behavior;
    • compatibility dengan Kubernetes lifecycle.

Kesalahan umum: menganggap Dockerfile hanya bagian CI/CD. Padahal Dockerfile menentukan banyak hal yang akan muncul sebagai incident production.

Contoh mapping:

Dockerfile concernProduction symptom
Base image terlalu besarImage pull lambat, surface CVE besar
Tidak pin digestBuild tidak reproducible
Running as rootGagal admission policy atau security finding
Wrong entrypointSignal SIGTERM tidak sampai ke JVM
COPY terlalu awalBuild cache selalu invalid
Tidak expose management portProbe salah target
JVM option hardcodedTidak cocok antar environment
Tidak punya writable temp pathRuntime error saat filesystem read-only
Alpine + native dependencyRuntime crash karena libc mismatch

2. Baseline Dockerfile untuk Java Service

Contoh baseline sederhana, bukan final standard universal:

# syntax=docker/dockerfile:1.7

FROM eclipse-temurin:17-jdk-jammy AS build
WORKDIR /workspace

COPY pom.xml ./
COPY src ./src

RUN --mount=type=cache,target=/root/.m2 \
    mvn -B -DskipTests package

FROM eclipse-temurin:17-jre-jammy AS runtime
WORKDIR /app

RUN groupadd --system app && useradd --system --gid app app

COPY --from=build /workspace/target/app.jar /app/app.jar

USER app:app
EXPOSE 8080

ENTRYPOINT ["java"]
CMD ["-jar", "/app/app.jar"]

Ini masih bisa diperbaiki, tetapi sudah memperlihatkan beberapa prinsip:

  • build dan runtime dipisahkan;
  • runtime image tidak membawa Maven/source code;
  • dependency Maven bisa memakai BuildKit cache;
  • process tidak berjalan sebagai root;
  • command memakai exec form, bukan shell form;
  • port eksplisit;
  • artifact final hanya app.jar.

3. Base Image: JDK vs JRE

JDK image

Dipakai untuk build stage.

Berisi:

  • compiler;
  • tools build;
  • jar/jlink/jdeps;
  • debugging tools tertentu;
  • ukuran lebih besar;
  • surface vulnerability lebih luas.

Cocok untuk:

  • mvn package;
  • gradle build;
  • compile annotation processor;
  • test stage;
  • custom runtime generation dengan jlink.

JRE image

Dipakai untuk runtime stage.

Berisi runtime Java tanpa full compiler/toolchain.

Cocok untuk:

  • menjalankan executable jar;
  • menjalankan app server embedded;
  • mengurangi ukuran image;
  • mengurangi attack surface.

Rule praktis

  • Jangan jalankan production app memakai image JDK kecuali ada alasan kuat.
  • Gunakan JDK di build stage.
  • Gunakan JRE, distroless, atau custom runtime di runtime stage.
  • Pastikan versi JDK build dan runtime kompatibel.

4. Base Image: Debian/Ubuntu Slim vs Alpine vs Distroless

Debian/Ubuntu slim

Kelebihan:

  • kompatibilitas tinggi;
  • glibc tersedia;
  • mudah debug;
  • banyak library native cocok;
  • umum untuk enterprise runtime.

Kekurangan:

  • lebih besar dibanding Alpine/distroless;
  • lebih banyak package berarti potensi CVE lebih besar;
  • perlu patching base image berkala.

Cocok untuk:

  • service enterprise biasa;
  • dependency native;
  • kebutuhan debugging lebih mudah;
  • transisi awal menuju container production.

Alpine

Kelebihan:

  • image kecil;
  • footprint rendah.

Kekurangan:

  • memakai musl libc, bukan glibc;
  • beberapa native library Java ecosystem bisa bermasalah;
  • DNS, timezone, certificate, font, compression, crypto, dan native dependency kadang punya edge case;
  • debugging bisa lebih tricky.

Cocok untuk:

  • service sederhana;
  • tim paham caveat musl;
  • sudah diuji lengkap dengan dependency runtime.

Tidak otomatis cocok untuk enterprise Java hanya karena kecil.

Distroless

Kelebihan:

  • sangat kecil;
  • attack surface rendah;
  • tidak membawa shell/package manager;
  • bagus untuk production hardening.

Kekurangan:

  • debugging langsung di container sulit;
  • perlu observability dan ephemeral debug container yang matang;
  • perlu build/runtime discipline lebih tinggi.

Cocok untuk:

  • workload production mature;
  • pipeline scanning/signing matang;
  • platform mendukung debugging tanpa shell;
  • aplikasi tidak bergantung pada tool OS runtime.

Decision table

PilihanKapan cocokRisiko utama
JDK runtimeButuh tool JDK di runtimeImage besar, CVE lebih banyak
JRE slimDefault enterprise pragmaticUkuran sedang, perlu patch base
AlpineButuh image kecil dan sudah diujimusl/native compatibility
DistrolessProduction hardening matangDebug sulit, tidak ada shell
Custom jlinkRuntime minimal khusus appBuild complexity, module analysis

5. Multi-Stage Build

Multi-stage build memisahkan environment build dan runtime.

Tanpa multi-stage:

FROM maven:3.9-eclipse-temurin-17
WORKDIR /app
COPY . .
RUN mvn package
CMD ["java", "-jar", "target/app.jar"]

Masalah:

  • source code ikut image runtime;
  • Maven cache ikut image runtime;
  • build tool ikut production image;
  • ukuran besar;
  • CVE lebih banyak;
  • boundary build/runtime kabur.

Dengan multi-stage:

FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /workspace
COPY pom.xml .
COPY src ./src
RUN mvn -B -DskipTests package

FROM eclipse-temurin:17-jre-jammy AS runtime
WORKDIR /app
COPY --from=build /workspace/target/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Lebih baik karena runtime image hanya membawa artifact yang diperlukan.


6. Build Context

Build context adalah seluruh file yang dikirim ke Docker daemon/build engine saat docker build.

Jika .dockerignore buruk, build context bisa membawa:

  • .git;
  • target/;
  • node_modules/;
  • local secrets;
  • IDE files;
  • test reports;
  • log files;
  • dump files;
  • credentials;
  • large binary artifacts.

Contoh .dockerignore:

.git
.gitignore
Dockerfile
README.md
*.log
.env
.env.*
target
build
out
.idea
.vscode
node_modules
coverage
*.iml

Catatan: jangan asal ignore file yang dibutuhkan build. .dockerignore harus selaras dengan cara COPY di Dockerfile.

Failure mode

Masalah build contextDampak
Context terlalu besarBuild lambat
Secret masuk contextRisiko leakage
target/ ikut contextArtifact lama bisa tidak sengaja tercopy
.git ikut contextInformasi internal bocor
.dockerignore terlalu agresifBuild gagal karena file hilang

7. Maven Dependency Cache

Docker layer cache bekerja berdasarkan instruksi. Jika kita copy seluruh source sebelum dependency resolve, setiap perubahan source akan membuat dependency download ulang.

Kurang baik:

COPY . .
RUN mvn package

Lebih baik:

COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B dependency:go-offline

COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B package

Dengan BuildKit cache:

  • dependency Maven tidak masuk final image;
  • build lebih cepat;
  • cache lebih stabil;
  • pipeline lebih efisien.

Namun perlu hati-hati:

  • cache tidak boleh menjadi sumber correctness;
  • dependency version harus eksplisit;
  • jangan mengandalkan snapshot dependency tanpa kontrol;
  • CI harus punya mode clean/reproducible build untuk release.

8. Layering Java Dependencies

Java executable jar sering berubah walau hanya satu class berubah. Jika seluruh fat jar dicopy sebagai satu file, layer cache runtime selalu invalid.

Untuk Spring Boot ada layertools. Untuk JAX-RS/Jakarta service tergantung packaging:

  • executable jar;
  • thin jar + lib directory;
  • WAR untuk app server;
  • shaded jar;
  • Quarkus/MicroProfile packaging;
  • custom distribution.

Pattern umum thin distribution:

COPY target/lib /app/lib
COPY target/app.jar /app/app.jar

Keuntungan:

  • dependency jar layer lebih stabil;
  • app jar layer lebih kecil;
  • image push/pull bisa lebih efisien;
  • vulnerability ownership lebih jelas.

Trade-off:

  • build packaging lebih kompleks;
  • classpath harus benar;
  • manifest dan entrypoint perlu rapi.

Contoh entrypoint thin jar:

ENTRYPOINT ["java", "-cp", "/app/app.jar:/app/lib/*", "com.example.Main"]

9. WAR Packaging vs Executable JAR

Enterprise JAX-RS service bisa berjalan dalam beberapa bentuk:

  1. Executable JAR

    • embedded server;
    • mudah containerized;
    • satu process per container;
    • cocok untuk Kubernetes.
  2. WAR di application server

    • deploy ke server seperti Tomcat, Jetty, WildFly, Payara, Open Liberty;
    • berguna jika organisasi punya standard app server;
    • image bisa lebih kompleks;
    • lifecycle app dan lifecycle server perlu dipahami.
  3. Thin JAR + lib

    • dependency terpisah;
    • layer lebih efisien;
    • classpath management lebih eksplisit.
  4. Native image

    • startup cepat;
    • memory lebih kecil;
    • build lebih kompleks;
    • reflection/config compatibility perlu diuji.

Untuk seri ini, default mental model: satu container menjalankan satu Java service process yang expose HTTP endpoint dan optional management endpoint.


10. Runtime User: Non-Root Container

Container default sering berjalan sebagai root. Ini tidak ideal.

Risiko root di container:

  • jika escape vulnerability terjadi, dampak lebih besar;
  • file yang dibuat di mounted volume bisa punya owner root;
  • admission policy bisa menolak pod;
  • melanggar security baseline enterprise;
  • sulit memenuhi compliance expectation.

Contoh:

RUN groupadd --system app && useradd --system --gid app app
USER app:app

Untuk image yang tidak punya useradd, bisa memakai numeric UID:

USER 10001:10001

Namun pastikan directory yang perlu ditulis sudah punya permission benar:

RUN mkdir -p /app /tmp/app && chown -R app:app /app /tmp/app

Runtime write path

Java app mungkin butuh menulis ke:

  • /tmp;
  • log file jika masih file-based;
  • local cache;
  • upload temp;
  • generated file;
  • keystore temporary;
  • native library extraction path.

Jika Kubernetes memakai readOnlyRootFilesystem: true, app harus punya writable mount khusus seperti emptyDir.


11. Working Directory

Gunakan WORKDIR agar command dan relative path jelas.

WORKDIR /app
COPY app.jar /app/app.jar

Masalah jika tidak jelas:

  • relative path salah;
  • config file tidak ditemukan;
  • log path salah;
  • keystore/truststore path salah;
  • process bekerja di /;
  • permission problem.

Rule:

  • pilih /app, /workspace, atau path standard internal;
  • jangan jalankan dari root filesystem /;
  • pastikan ownership sesuai runtime user.

12. EXPOSE Port

EXPOSE tidak membuka port secara otomatis di Kubernetes. Ia hanya dokumentasi image metadata.

EXPOSE 8080

Tetap perlu di Kubernetes:

ports:
  - name: http
    containerPort: 8080

Lalu Service:

ports:
  - name: http
    port: 80
    targetPort: http

Senior review concern:

  • app listen di port berapa?
  • containerPort sesuai?
  • Service targetPort sesuai?
  • ingress backend service port sesuai?
  • management port dipisah atau sama?
  • probe menargetkan port yang benar?

13. ENTRYPOINT vs CMD

ENTRYPOINT mendefinisikan executable utama. CMD memberikan default arguments.

Recommended exec form:

ENTRYPOINT ["java"]
CMD ["-jar", "/app/app.jar"]

Atau:

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Hindari shell form:

CMD java -jar /app/app.jar

Risiko shell form:

  • process utama menjadi shell;
  • signal handling bisa buruk;
  • SIGTERM tidak selalu diteruskan dengan benar;
  • graceful shutdown bisa gagal;
  • zombie process handling bisa bermasalah.

Jika butuh script entrypoint, pastikan script memakai exec:

#!/bin/sh
set -e
exec java ${JAVA_OPTS} -jar /app/app.jar

Tanpa exec, shell tetap menjadi PID 1 dan Java menjadi child process.


14. Environment Variables dan JVM Options

Jangan hardcode semua JVM option di Dockerfile. Environment berbeda bisa butuh tuning berbeda.

Pattern umum:

ENV JAVA_OPTS=""
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]

Namun ini memakai shell. Lebih fleksibel tetapi perlu hati-hati dengan quoting dan signal handling. Alternatif lebih aman adalah wrapper script yang memakai exec.

Contoh:

COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

docker-entrypoint.sh:

#!/bin/sh
set -eu
exec java ${JAVA_OPTS:-} -jar /app/app.jar

Kubernetes:

env:
  - name: JAVA_OPTS
    value: >-
      -XX:MaxRAMPercentage=75
      -XX:+ExitOnOutOfMemoryError
      -Djava.security.egd=file:/dev/urandom

Review concern

  • Apakah JAVA_OPTS bisa dikontrol per environment?
  • Apakah default aman?
  • Apakah ada option yang bertentangan dengan container limit?
  • Apakah ExitOnOutOfMemoryError dipakai agar pod restart saat OOM internal?
  • Apakah GC log diarahkan ke stdout/stderr atau observability path yang benar?

15. Healthcheck di Dockerfile vs Kubernetes Probe

Docker punya instruksi:

HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1

Namun di Kubernetes, health behavior biasanya dikontrol oleh:

  • livenessProbe;
  • readinessProbe;
  • startupProbe.

Untuk production Kubernetes, probe di manifest lebih penting daripada HEALTHCHECK Dockerfile.

Pertimbangan:

  • Dockerfile HEALTHCHECK berguna untuk local Docker/Compose;
  • Kubernetes tidak selalu memakai Docker healthcheck sebagai probe;
  • jangan mengandalkan Dockerfile healthcheck untuk production routing;
  • hindari memasukkan curl hanya demi healthcheck karena menambah package dan CVE.

Jika memakai distroless, curl tidak tersedia. Gunakan probe Kubernetes HTTP langsung.


16. Logging: Jangan Tulis Log ke File sebagai Default

Container-native logging berarti aplikasi menulis ke stdout/stderr.

Baik:

Java app -> stdout/stderr -> container runtime -> node log collector -> centralized logging

Kurang baik:

Java app -> /app/logs/app.log inside container

Risiko file logging:

  • log hilang saat container restart;
  • disk ephemeral penuh;
  • log collector tidak mengambil file;
  • permission issue non-root;
  • rotasi log tidak jelas;
  • duplicate logging jika stdout dan file aktif.

Untuk enterprise, file log hanya boleh jika memang ada sidecar/agent/volume pattern yang disepakati.


17. TLS, Truststore, dan Certificate di Image

Java service sering memanggil:

  • PostgreSQL TLS;
  • Kafka TLS;
  • RabbitMQ TLS;
  • Redis TLS;
  • internal HTTPS API;
  • AWS/Azure endpoints;
  • on-prem service dengan private CA.

Pertanyaan penting:

  • Apakah CA certificate berasal dari base image?
  • Apakah private CA dimasukkan saat build atau mounted saat runtime?
  • Apakah truststore Java memakai OS truststore atau custom truststore?
  • Apakah certificate rotation memerlukan rebuild image?
  • Apakah secret/cert dimasukkan ke image secara tidak sengaja?

Prinsip:

  • jangan bake secret ke image;
  • hindari bake certificate environment-specific ke image kecuali itu corporate root CA standard dan dikelola governance;
  • prefer mount melalui Secret/CSI/secret manager jika cert bersifat environment-specific;
  • dokumentasikan truststore path dan reload behavior.

18. Timezone dan Locale

Container sering default UTC. Itu biasanya baik untuk production.

Risiko:

  • aplikasi mengasumsikan local timezone;
  • timestamp log tidak konsisten;
  • batch schedule berbeda;
  • date parsing bergantung locale;
  • report/settlement salah tanggal.

Best practice:

  • gunakan UTC untuk runtime dan logs;
  • timezone bisnis ditangani eksplisit di aplikasi;
  • jangan bergantung pada timezone host/node;
  • jika perlu timezone data, pastikan base image memiliki tzdata atau Java tzdb cukup.

Review checklist:

  • TZ env var ada atau tidak?
  • aplikasi memakai Instant, OffsetDateTime, atau local time tanpa zone?
  • log timestamp memakai UTC?
  • schedule job dipengaruhi timezone?

19. File Descriptor dan Native Dependencies

Java backend production bisa memakai banyak file descriptor untuk:

  • HTTP connections;
  • DB pool;
  • Kafka connections;
  • RabbitMQ channels;
  • Redis connections;
  • log files;
  • TLS sockets;
  • DNS resolver;
  • ephemeral files.

Dockerfile tidak biasanya mengatur ulimit. Di Kubernetes, limit FD lebih terkait node/runtime/security context. Namun image harus tidak membuat asumsi salah.

Native dependencies juga bisa muncul dari:

  • compression libraries;
  • Netty native transport;
  • TLS/OpenSSL;
  • Kerberos;
  • database driver native bits;
  • font/rendering library;
  • image/PDF processing;
  • timezone/locale;
  • OS certificate store.

Base image minimal bisa membuat dependency ini hilang.


20. Image Tag, Version, dan Digest dari Dockerfile Perspective

Dockerfile biasanya memakai:

FROM eclipse-temurin:17-jre-jammy

Masalah: tag bisa berubah.

Untuk reproducibility lebih kuat:

FROM eclipse-temurin:17-jre-jammy@sha256:<digest>

Trade-off:

  • digest pinning membuat build reproducible;
  • tetapi base image patch tidak otomatis masuk;
  • perlu proses update digest berkala;
  • vulnerability patching harus dikelola pipeline.

Untuk enterprise, yang penting bukan hanya pin atau tidak pin. Yang penting adalah governance:

  • siapa owner base image?
  • kapan update dilakukan?
  • bagaimana CVE ditriage?
  • bagaimana image dipromosikan?
  • bagaimana digest final dicatat di deployment?

21. Dockerfile Anti-Patterns

Anti-pattern 1: Copy semua file tanpa kontrol

COPY . .

Risiko:

  • secret ikut;
  • cache buruk;
  • artifact lama ikut;
  • image tidak deterministic.

Anti-pattern 2: Running as root

USER root

Atau tidak ada USER sama sekali.

Risiko:

  • security finding;
  • admission rejection;
  • privilege escalation impact lebih besar.

Anti-pattern 3: Install tool debug di runtime image

RUN apt-get update && apt-get install -y curl vim netcat telnet

Risiko:

  • image besar;
  • CVE meningkat;
  • tool abuse;
  • tidak cocok dengan hardening.

Gunakan ephemeral debug container atau dedicated debug image bila platform mendukung.

Anti-pattern 4: Secret saat build

ENV DB_PASSWORD=secret

Atau:

COPY prod-secret.json /app/secret.json

Risiko:

  • secret masuk layer image;
  • walau dihapus di layer berikutnya, secret masih bisa ada di history/layer;
  • registry menjadi secret leakage source.

Anti-pattern 5: Shell entrypoint tanpa exec

java -jar app.jar

Risiko:

  • SIGTERM tidak sampai benar;
  • rolling update tidak graceful;
  • pod dipaksa SIGKILL.

Anti-pattern 6: Latest tag

FROM eclipse-temurin:latest

Risiko:

  • build berubah tanpa commit berubah;
  • vulnerability baseline tidak stabil;
  • sulit incident forensic.

22. Production-Grade Dockerfile Example

Contoh ini bukan standard final CSG. Anggap sebagai reference pattern yang harus disesuaikan dengan internal build, registry, security, dan runtime policy.

# syntax=docker/dockerfile:1.7

ARG JAVA_VERSION=17
ARG BASE_IMAGE=eclipse-temurin:${JAVA_VERSION}-jre-jammy
ARG BUILD_IMAGE=maven:3.9-eclipse-temurin-${JAVA_VERSION}

FROM ${BUILD_IMAGE} AS build
WORKDIR /workspace

COPY pom.xml ./
COPY .mvn ./.mvn
COPY mvnw ./mvnw

RUN --mount=type=cache,target=/root/.m2 \
    ./mvnw -B -DskipTests dependency:go-offline

COPY src ./src

RUN --mount=type=cache,target=/root/.m2 \
    ./mvnw -B -DskipTests package

FROM ${BASE_IMAGE} AS runtime
WORKDIR /app

RUN groupadd --system app \
    && useradd --system --gid app --home-dir /app app \
    && mkdir -p /app /tmp/app \
    && chown -R app:app /app /tmp/app

COPY --from=build --chown=app:app /workspace/target/app.jar /app/app.jar
COPY --chown=app:app docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh

RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh

USER app:app

ENV JAVA_OPTS="" \
    APP_PORT="8080" \
    TMPDIR="/tmp/app"

EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

docker-entrypoint.sh:

#!/bin/sh
set -eu

exec java ${JAVA_OPTS:-} -jar /app/app.jar

Review notes:

  • tidak memasukkan secret;
  • tidak membawa Maven ke runtime;
  • runtime non-root;
  • writable temp eksplisit;
  • BuildKit cache dipakai;
  • entrypoint memakai exec;
  • JVM options bisa dikontrol via env;
  • port terdokumentasi;
  • masih perlu scanning, digest pinning, SBOM, signing, dan Kubernetes manifest yang benar.

23. Dockerfile untuk WAR Deployment

Jika aplikasi dikemas sebagai WAR di app server:

FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /workspace
COPY pom.xml ./
COPY src ./src
RUN mvn -B -DskipTests package

FROM tomcat:10.1-jre17-temurin-jammy AS runtime
RUN groupadd --system app && useradd --system --gid app app
COPY --from=build /workspace/target/my-service.war /usr/local/tomcat/webapps/ROOT.war
USER app:app
EXPOSE 8080
CMD ["catalina.sh", "run"]

Review concern:

  • apakah base app server patching jelas?
  • apakah default apps dihapus?
  • apakah server.xml dikontrol?
  • apakah access log ke stdout?
  • apakah shutdown Tomcat/WildFly graceful?
  • apakah management endpoint terpisah?
  • apakah filesystem permission cocok dengan non-root?

24. Dockerfile untuk Thin JAR

Jika build menghasilkan dependency terpisah:

FROM eclipse-temurin:17-jre-jammy
WORKDIR /app

RUN groupadd --system app && useradd --system --gid app app

COPY --chown=app:app target/lib /app/lib
COPY --chown=app:app target/app.jar /app/app.jar

USER app:app
EXPOSE 8080

ENTRYPOINT ["java", "-cp", "/app/app.jar:/app/lib/*", "com.company.service.Main"]

Keuntungan:

  • dependency layer lebih stabil;
  • perubahan source code tidak selalu mengubah dependency layer;
  • scanning dependency jar lebih eksplisit.

Risiko:

  • classpath order;
  • missing dependency;
  • duplicate version;
  • main class salah;
  • manifest tidak digunakan.

25. Dockerfile dan Kubernetes Manifest Harus Selaras

Dockerfile tidak berdiri sendiri. Ia harus selaras dengan Deployment, Service, Ingress, ConfigMap, Secret, dan security policy.

DockerfileKubernetes concern
USER appsecurityContext.runAsNonRoot
EXPOSE 8080containerPort, Service targetPort
ENTRYPOINTgraceful SIGTERM behavior
ENV JAVA_OPTSDeployment env var / Helm values
/tmp/app writableemptyDir, readOnlyRootFilesystem
no shell/distrolessdebug via ephemeral container
base image CA storeSecret/ConfigMap/CSI cert mount
stdout logginglog collector config

Mismatch contoh:

  • Dockerfile expose 8080, app listen 8081, Service target 8080 → service unreachable.
  • Runtime non-root, app write ke /app/logs, directory root-owned → CrashLoopBackOff.
  • Shell entrypoint tanpa exec, Kubernetes delete pod → request putus saat rollout.
  • Distroless image, runbook menyuruh kubectl exec sh → debug gagal.

26. Failure Modes dari Dockerfile Buruk

26.1 Image build fails

Kemungkinan:

  • build context salah;
  • .dockerignore menghapus file penting;
  • Maven repository unreachable;
  • dependency snapshot tidak tersedia;
  • base image tidak bisa pull;
  • BuildKit disabled;
  • permission script entrypoint tidak executable.

Debug:

docker build --no-cache -t app:test .
docker build --progress=plain -t app:test .

26.2 Container exits immediately

Kemungkinan:

  • command salah;
  • jar path salah;
  • main class salah;
  • permission denied;
  • missing config;
  • Java process tidak bisa bind port;
  • app crash saat startup.

Debug:

docker run --rm app:test
docker run --rm --entrypoint sh app:test

Jika distroless, gunakan debug image atau Kubernetes ephemeral container.

26.3 Permission denied

Kemungkinan:

  • non-root user tidak punya akses ke /app;
  • script entrypoint tidak executable;
  • app write ke directory root-owned;
  • mounted volume override ownership.

Debug:

docker run --rm app:test ls -la /app

26.4 SIGTERM tidak graceful

Kemungkinan:

  • shell form CMD;
  • wrapper script tanpa exec;
  • app tidak handle shutdown;
  • server tidak drain request;
  • consumer tidak close.

Debug:

docker run --name app app:test

docker stop --time 30 app

Observe apakah app menerima shutdown event dan exit clean.

26.5 ImagePullBackOff di Kubernetes

Kemungkinan:

  • image tag salah;
  • registry auth salah;
  • image belum dipush;
  • registry unreachable;
  • private registry certificate issue;
  • architecture mismatch;
  • rate limit.

Dockerfile sendiri bukan satu-satunya penyebab, tetapi tag/build/push governance berkaitan erat.


27. Security Review Checklist

Saat review Dockerfile, tanyakan:

  1. Apakah base image berasal dari registry yang disetujui?
  2. Apakah versi base image eksplisit?
  3. Apakah digest dipin untuk release image?
  4. Apakah runtime image minimal dan tidak membawa build tools?
  5. Apakah image berjalan sebagai non-root?
  6. Apakah ada secret yang masuk Dockerfile, build args, env, atau copied file?
  7. Apakah package manager cache dibersihkan jika install package?
  8. Apakah tool debug tidak perlu dibuang dari runtime image?
  9. Apakah image discan CVE?
  10. Apakah SBOM dibuat?
  11. Apakah artifact ditandatangani?
  12. Apakah Docker socket tidak di-mount ke container?
  13. Apakah certificate/truststore dikelola aman?
  14. Apakah writable path dibatasi?
  15. Apakah cocok dengan Pod Security Standards?

28. Performance Review Checklist

  1. Apakah image terlalu besar?
  2. Apakah layer cache efisien?
  3. Apakah dependency jar dipisah dari app code jika perlu?
  4. Apakah startup time diukur?
  5. Apakah base image memperlambat cold start karena size?
  6. Apakah JVM options cocok dengan resource limit?
  7. Apakah app menulis log berlebihan ke stdout?
  8. Apakah /tmp cukup untuk workload?
  9. Apakah DNS/cert/timezone dependency tersedia?
  10. Apakah native dependency cocok dengan OS image?

29. Cost Review Checklist

Dockerfile juga berpengaruh ke biaya:

  • image besar → registry storage naik;
  • image besar → pull time naik;
  • pull time naik → rollout lambat;
  • rollout lambat → capacity buffer lebih besar;
  • bad layering → push/pull selalu besar;
  • base image CVE banyak → patch cycle lebih mahal;
  • runtime boros memory → node cost naik.

Checklist:

  1. Berapa ukuran final image?
  2. Layer terbesar apa?
  3. Apakah build artifact membawa file tidak perlu?
  4. Apakah dependency duplicated?
  5. Apakah image retention policy ada?
  6. Apakah image dipromosikan atau rebuild per environment?
  7. Apakah digest yang sama digunakan antar environment?

30. Observability Review Checklist

Dockerfile harus mendukung observability runtime.

  1. App log ke stdout/stderr?
  2. Apakah log file path tidak tersembunyi di container?
  3. Apakah timezone log konsisten?
  4. Apakah JVM crash log diarahkan ke writable path?
  5. Apakah heap dump policy jelas?
  6. Apakah image punya label build metadata?
  7. Apakah commit SHA/version tersedia sebagai env atau label?
  8. Apakah actuator/health/metrics endpoint bukan tanggung jawab Dockerfile tapi diselaraskan dengan manifest?

Contoh OCI labels:

LABEL org.opencontainers.image.title="quote-order-service" \
      org.opencontainers.image.description="Java JAX-RS Quote and Order service" \
      org.opencontainers.image.source="internal-repository-url" \
      org.opencontainers.image.revision="${VCS_REF}" \
      org.opencontainers.image.version="${APP_VERSION}"

Jangan masukkan URL internal sensitif jika image bisa keluar dari boundary internal.


31. Internal Verification Checklist

Untuk konteks CSG/team, jangan asumsi. Verifikasi:

Dockerfile dan build

  • Di mana Dockerfile service berada?
  • Apakah ada satu Dockerfile per service atau shared base?
  • Apakah memakai Maven, Gradle, atau build system lain?
  • Apakah memakai executable jar, thin jar, WAR, Quarkus, MicroProfile, atau app server?
  • Apakah ada .dockerignore?
  • Apakah BuildKit aktif di CI?
  • Apakah dependency cache dipakai?
  • Apakah build reproducible?

Base image dan registry

  • Registry internal apa yang dipakai?
  • Apakah base image berasal dari curated internal image?
  • Apakah ECR, ACR, atau private registry dipakai?
  • Apakah image tag immutable?
  • Apakah digest dipakai di manifest?
  • Apakah image promotion dilakukan atau rebuild per environment?

Runtime

  • Apakah container berjalan sebagai non-root?
  • Apakah root filesystem read-only?
  • Apakah app butuh writable /tmp?
  • Apakah timezone diset?
  • Apakah CA/truststore custom dipakai?
  • Apakah JVM options dikelola via env/Helm values?
  • Apakah graceful shutdown sudah diuji?

Security

  • Apakah vulnerability scan wajib?
  • Apakah SBOM dibuat?
  • Apakah image signing/admission policy ada?
  • Apakah secret pernah masuk build context?
  • Apakah Docker socket pernah di-mount?
  • Apakah runtime image membawa shell/tools?

Kubernetes alignment

  • Apakah EXPOSE sesuai containerPort?
  • Apakah app listen port sesuai Service targetPort?
  • Apakah probes menargetkan endpoint yang benar?
  • Apakah JAVA_OPTS dari Helm/Kustomize overlay?
  • Apakah securityContext cocok dengan Dockerfile USER?
  • Apakah runbook debugging cocok jika image distroless?

32. PR Review Checklist

Gunakan checklist ini saat review PR yang mengubah Dockerfile:

[ ] Base image explicit dan disetujui
[ ] Tidak memakai latest tag
[ ] Build stage dan runtime stage dipisah
[ ] Runtime image tidak membawa source code/build tool
[ ] .dockerignore tersedia dan aman
[ ] Dependency cache efisien tetapi tidak merusak reproducibility
[ ] Artifact final jelas
[ ] Container berjalan sebagai non-root
[ ] Working directory eksplisit
[ ] Writable path eksplisit
[ ] ENTRYPOINT/CMD memakai exec form atau script dengan exec
[ ] Tidak ada secret di Dockerfile/build context/layer
[ ] Port sesuai manifest Kubernetes
[ ] JVM options tidak hardcoded secara kaku
[ ] Logging ke stdout/stderr
[ ] Health behavior diselaraskan dengan Kubernetes probes
[ ] Truststore/certificate strategy jelas
[ ] Image size masuk akal
[ ] Vulnerability scan/SBOM/signing masuk pipeline
[ ] Debugging approach jelas untuk slim/distroless image
[ ] Rollout/graceful shutdown sudah dipertimbangkan

33. Key Takeaways

  1. Dockerfile adalah runtime contract, bukan hanya build script.
  2. Multi-stage build adalah default untuk Java production image.
  3. Runtime image sebaiknya tidak membawa compiler, Maven, source code, atau secret.
  4. Base image harus dipilih berdasarkan compatibility, security, operability, dan governance, bukan ukuran saja.
  5. Non-root runtime adalah baseline security modern.
  6. ENTRYPOINT/CMD memengaruhi SIGTERM dan graceful shutdown.
  7. Dockerfile harus selaras dengan Kubernetes Deployment, Service, probes, securityContext, ConfigMap, Secret, dan observability.
  8. Image yang kecil tetapi sulit di-debug bisa memperlambat incident response jika platform belum siap.
  9. Tag, digest, scanning, SBOM, signing, dan promotion adalah bagian dari production image lifecycle.
  10. Senior engineer harus bisa membaca Dockerfile sebagai prediksi failure production.

34. Latihan Praktis

Untuk satu service Java/JAX-RS yang kamu punya aksesnya:

  1. Buka Dockerfile.
  2. Identifikasi build stage dan runtime stage.
  3. Catat base image dan versinya.
  4. Cek apakah runtime memakai JDK atau JRE.
  5. Cek apakah container berjalan sebagai root.
  6. Cek file apa saja yang dicopy ke image.
  7. Cek .dockerignore.
  8. Cek entrypoint dan signal handling.
  9. Cek port yang diexpose.
  10. Bandingkan dengan Kubernetes Deployment dan Service.
  11. Cek apakah JVM options berasal dari env/Helm values.
  12. Cek apakah image discan dan dipromosikan.
  13. Tulis minimal 5 risiko yang mungkin muncul dari Dockerfile tersebut.

Output latihan yang bagus:

Service: <nama-service>
Image: <registry>/<repo>:<tag>
Base image: <base>
Runtime user: <uid/user>
Entrypoint: <entrypoint>
Port: <port>
Top risks:
1. ...
2. ...
3. ...
Recommended fixes:
1. ...
2. ...
3. ...
Internal questions:
1. ...
2. ...
3. ...

35. Transisi ke Part 004

Setelah Dockerfile benar, pertanyaan berikutnya adalah: bagaimana JVM benar-benar berperilaku saat dibatasi oleh container?

Itu mencakup:

  • heap sizing;
  • memory limit;
  • CPU quota;
  • CPU throttling;
  • GC behavior;
  • thread stack;
  • metaspace;
  • direct memory;
  • file descriptor;
  • SIGTERM;
  • graceful shutdown;
  • OOMKilled.

Itulah fokus Part 004.

Lesson Recap

You just completed lesson 03 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.