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.
Part 003 — Dockerfile for Java/JAX-RS Services
Fokus part ini: bagaimana menulis dan mereview
Dockerfileuntuk 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, atauOOMKilled.
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:
-
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.
-
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 concern | Production symptom |
|---|---|
| Base image terlalu besar | Image pull lambat, surface CVE besar |
| Tidak pin digest | Build tidak reproducible |
| Running as root | Gagal admission policy atau security finding |
| Wrong entrypoint | Signal SIGTERM tidak sampai ke JVM |
| COPY terlalu awal | Build cache selalu invalid |
| Tidak expose management port | Probe salah target |
| JVM option hardcoded | Tidak cocok antar environment |
| Tidak punya writable temp path | Runtime error saat filesystem read-only |
| Alpine + native dependency | Runtime 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
| Pilihan | Kapan cocok | Risiko utama |
|---|---|---|
| JDK runtime | Butuh tool JDK di runtime | Image besar, CVE lebih banyak |
| JRE slim | Default enterprise pragmatic | Ukuran sedang, perlu patch base |
| Alpine | Butuh image kecil dan sudah diuji | musl/native compatibility |
| Distroless | Production hardening matang | Debug sulit, tidak ada shell |
| Custom jlink | Runtime minimal khusus app | Build 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 context | Dampak |
|---|---|
| Context terlalu besar | Build lambat |
| Secret masuk context | Risiko leakage |
target/ ikut context | Artifact lama bisa tidak sengaja tercopy |
.git ikut context | Informasi internal bocor |
.dockerignore terlalu agresif | Build 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:
-
Executable JAR
- embedded server;
- mudah containerized;
- satu process per container;
- cocok untuk Kubernetes.
-
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.
-
Thin JAR + lib
- dependency terpisah;
- layer lebih efisien;
- classpath management lebih eksplisit.
-
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_OPTSbisa dikontrol per environment? - Apakah default aman?
- Apakah ada option yang bertentangan dengan container limit?
- Apakah
ExitOnOutOfMemoryErrordipakai 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
HEALTHCHECKberguna untuk local Docker/Compose; - Kubernetes tidak selalu memakai Docker healthcheck sebagai probe;
- jangan mengandalkan Dockerfile healthcheck untuk production routing;
- hindari memasukkan
curlhanya 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
tzdataatau Java tzdb cukup.
Review checklist:
TZenv 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.
| Dockerfile | Kubernetes concern |
|---|---|
USER app | securityContext.runAsNonRoot |
EXPOSE 8080 | containerPort, Service targetPort |
ENTRYPOINT | graceful SIGTERM behavior |
ENV JAVA_OPTS | Deployment env var / Helm values |
/tmp/app writable | emptyDir, readOnlyRootFilesystem |
| no shell/distroless | debug via ephemeral container |
| base image CA store | Secret/ConfigMap/CSI cert mount |
| stdout logging | log 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;
.dockerignoremenghapus 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:
- Apakah base image berasal dari registry yang disetujui?
- Apakah versi base image eksplisit?
- Apakah digest dipin untuk release image?
- Apakah runtime image minimal dan tidak membawa build tools?
- Apakah image berjalan sebagai non-root?
- Apakah ada secret yang masuk Dockerfile, build args, env, atau copied file?
- Apakah package manager cache dibersihkan jika install package?
- Apakah tool debug tidak perlu dibuang dari runtime image?
- Apakah image discan CVE?
- Apakah SBOM dibuat?
- Apakah artifact ditandatangani?
- Apakah Docker socket tidak di-mount ke container?
- Apakah certificate/truststore dikelola aman?
- Apakah writable path dibatasi?
- Apakah cocok dengan Pod Security Standards?
28. Performance Review Checklist
- Apakah image terlalu besar?
- Apakah layer cache efisien?
- Apakah dependency jar dipisah dari app code jika perlu?
- Apakah startup time diukur?
- Apakah base image memperlambat cold start karena size?
- Apakah JVM options cocok dengan resource limit?
- Apakah app menulis log berlebihan ke stdout?
- Apakah
/tmpcukup untuk workload? - Apakah DNS/cert/timezone dependency tersedia?
- 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:
- Berapa ukuran final image?
- Layer terbesar apa?
- Apakah build artifact membawa file tidak perlu?
- Apakah dependency duplicated?
- Apakah image retention policy ada?
- Apakah image dipromosikan atau rebuild per environment?
- Apakah digest yang sama digunakan antar environment?
30. Observability Review Checklist
Dockerfile harus mendukung observability runtime.
- App log ke stdout/stderr?
- Apakah log file path tidak tersembunyi di container?
- Apakah timezone log konsisten?
- Apakah JVM crash log diarahkan ke writable path?
- Apakah heap dump policy jelas?
- Apakah image punya label build metadata?
- Apakah commit SHA/version tersedia sebagai env atau label?
- 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
EXPOSEsesuaicontainerPort? - Apakah app listen port sesuai Service targetPort?
- Apakah probes menargetkan endpoint yang benar?
- Apakah
JAVA_OPTSdari 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
- Dockerfile adalah runtime contract, bukan hanya build script.
- Multi-stage build adalah default untuk Java production image.
- Runtime image sebaiknya tidak membawa compiler, Maven, source code, atau secret.
- Base image harus dipilih berdasarkan compatibility, security, operability, dan governance, bukan ukuran saja.
- Non-root runtime adalah baseline security modern.
- ENTRYPOINT/CMD memengaruhi SIGTERM dan graceful shutdown.
- Dockerfile harus selaras dengan Kubernetes Deployment, Service, probes, securityContext, ConfigMap, Secret, dan observability.
- Image yang kecil tetapi sulit di-debug bisa memperlambat incident response jika platform belum siap.
- Tag, digest, scanning, SBOM, signing, dan promotion adalah bagian dari production image lifecycle.
- Senior engineer harus bisa membaca Dockerfile sebagai prediksi failure production.
34. Latihan Praktis
Untuk satu service Java/JAX-RS yang kamu punya aksesnya:
- Buka Dockerfile.
- Identifikasi build stage dan runtime stage.
- Catat base image dan versinya.
- Cek apakah runtime memakai JDK atau JRE.
- Cek apakah container berjalan sebagai root.
- Cek file apa saja yang dicopy ke image.
- Cek
.dockerignore. - Cek entrypoint dan signal handling.
- Cek port yang diexpose.
- Bandingkan dengan Kubernetes Deployment dan Service.
- Cek apakah JVM options berasal dari env/Helm values.
- Cek apakah image discan dan dipromosikan.
- 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.
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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.