Tampilan
Kenapa penting
Di tim backend modern, "jalan di laptop saya" bukan bukti apa pun. Aplikasi Spring Boot kamu akan dijalankan di CI, di server staging, dan di cluster production — masing-masing dengan OS, versi Java, dan konfigurasi berbeda. Docker membungkus aplikasi beserta runtime-nya ke dalam satu image yang berjalan identik di mana pun. Hampir semua platform deploy (Kubernetes, Cloud Run, ECS, Fly.io) menerima image container sebagai unit deploy, jadi menulis Dockerfile yang kecil, cepat di-build, dan aman adalah keterampilan dasar yang ditanyakan di interview dan dipakai setiap minggu di pekerjaan.
Konsep inti
Container vs virtual machine
| Aspek | Virtual machine | Container |
|---|---|---|
| Isolasi | OS tamu lengkap di atas hypervisor | proses biasa yang diisolasi kernel (namespace & cgroup) |
| Ukuran | gigabyte (OS penuh) | puluhan–ratusan megabyte |
| Waktu start | puluhan detik–menit | sepersekian detik + waktu start JVM |
| Kernel | punya kernel sendiri | berbagi kernel host |
Container bukan VM mini. Ia adalah proses Linux yang "melihat" filesystem, jaringan, dan daftar prosesnya sendiri, dengan batas CPU dan memori dari cgroup. Karena berbagi kernel, container ringan — tetapi isolasinya juga lebih tipis daripada VM.
Image, layer, dan container
- Image: template read-only, tersusun dari tumpukan layer. Setiap instruksi
FROM,COPY,RUNdi Dockerfile menghasilkan satu layer. - Container: instance image yang sedang berjalan, dengan layer tulis tipis di atasnya (hilang saat container dihapus).
- Registry: tempat menyimpan image (Docker Hub, GHCR, ECR).
Docker menyimpan cache per layer. Bila instruksi dan file inputnya tidak berubah, layer lama dipakai ulang. Konsekuensinya: urutkan instruksi dari yang paling jarang berubah (base image, dependency) ke yang paling sering berubah (kode aplikasi).
Dockerfile multi-stage
Multi-stage build memisahkan tahap build (butuh JDK dan Maven) dari tahap runtime (cukup JRE). Hanya hasil akhir yang disalin ke image runtime, sehingga compiler, cache Maven, dan source code tidak ikut terbawa.
Contoh kecil yang benar-benar dibangun dan dijalankan — aplikasi Java satu file:
java
public class App {
public static void main(String[] args) {
long maxHeapMb = Runtime.getRuntime().maxMemory() / (1024 * 1024);
System.out.println("Halo dari container! user=" + System.getProperty("user.name"));
System.out.println("Java " + Runtime.version().feature() + ", max heap " + maxHeapMb + " MB");
}
}text
# Dockerfile
# Tahap 1: build
FROM eclipse-temurin:25-jdk AS build
WORKDIR /src
COPY src/App.java .
RUN javac -d out App.java
# Tahap 2: runtime
FROM eclipse-temurin:25-jre
RUN useradd --system --uid 10001 appuser
WORKDIR /app
COPY --from=build /src/out/ ./
USER appuser
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75"
ENTRYPOINT ["java", "-cp", "/app", "App"]bash
docker build -t jalur-docker-demo .
docker run --rm --memory=512m jalur-docker-demotext
Picked up JAVA_TOOL_OPTIONS: -XX:MaxRAMPercentage=75
Halo dari container! user=appuser
Java 25, max heap 371 MBBaris pertama dicetak JVM (ke stderr) sebagai tanda setelan JAVA_TOOL_OPTIONS terbaca. Perhatikan tiga hal: user-nya appuser (bukan root), heap maksimum mengikuti limit memori container (sekitar 75% dari 512 MB), dan ENTRYPOINT memakai bentuk exec (array JSON) sehingga java menjadi PID 1 dan menerima sinyal SIGTERM saat container dihentikan.
Dockerfile multi-stage untuk Spring Boot
Untuk proyek Maven, salin pom.xml dan Maven Wrapper lebih dulu agar layer dependency bisa di-cache:
text
# Dockerfile — Potongan untuk proyek Spring Boot 4 + Maven
FROM eclipse-temurin:25-jdk AS build
WORKDIR /workspace
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw -q dependency:go-offline
COPY src src
RUN ./mvnw -q -DskipTests package \
&& cp target/*.jar application.jar \
&& java -Djarmode=tools -jar application.jar extract --layers --destination extracted
FROM eclipse-temurin:25-jre
RUN useradd --system --uid 10001 spring
WORKDIR /application
COPY --from=build /workspace/extracted/dependencies/ ./
COPY --from=build /workspace/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/extracted/application/ ./
USER spring
EXPOSE 8080
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75"
ENTRYPOINT ["java", "-jar", "application.jar"]Kenapa test dilewati di Dockerfile?
-DskipTests di sini disengaja: test dijalankan di pipeline CI sebelum image dibangun (dibahas di bab CI/CD). Test berbasis Testcontainers juga butuh akses Docker, yang biasanya tidak tersedia di dalam docker build.
Layered jar
Fat jar Spring Boot berisi ratusan dependency dan kode kamu dalam satu file. Bila jar disalin utuh, satu baris perubahan membuat seluruh layer (puluhan MB) dibangun dan di-push ulang. Mode tools membongkar jar menjadi empat folder layer:
| Layer | Isi | Seberapa sering berubah |
|---|---|---|
dependencies | library rilis (Spring, Jackson, driver) | jarang |
spring-boot-loader | kode loader Boot | sangat jarang |
snapshot-dependencies | dependency versi -SNAPSHOT | kadang |
application | class & resource milikmu + application.jar | setiap commit |
Kamu bisa melihat daftarnya dengan java -Djarmode=tools -jar application.jar list-layers. Hasil extract juga ramah untuk CDS/AOT cache, yang mempercepat start JVM.
Buildpacks: tanpa Dockerfile
Spring Boot Maven Plugin bisa membangun image langsung memakai Cloud Native Buildpacks:
bash
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=toko/order-service:1.0.0Buildpacks memilih JRE, memakai layered jar, menjalankan aplikasi sebagai user non-root, dan menghitung setelan memori JVM secara otomatis. Kelebihannya: konsisten dan minim pemeliharaan. Kekurangannya: kontrol lebih sedikit dan butuh akses ke Docker daemon. Banyak tim memakai Buildpacks sebagai default dan beralih ke Dockerfile bila butuh kustomisasi.
.dockerignore dan ukuran image
COPY . . atau COPY src src mengirim build context ke daemon. Tanpa .dockerignore, folder target/, .git/, dan file IDE ikut terkirim — build lambat dan cache sering batal.
text
# .dockerignore
target/
.git/
.idea/
*.log
.envTips merampingkan image: pakai JRE (bukan JDK) di runtime, multi-stage, jangan memasang paket yang tidak dipakai, dan gabungkan perintah RUN yang membuat file sementara. Cek ukuran dengan docker image ls.
Memori JVM di container
JVM modern membaca limit cgroup. Tanpa setelan, heap maksimum default hanya 25% dari memori yang tersedia — boros untuk container 512 MB. -XX:MaxRAMPercentage=75 memberi heap 75% limit dan menyisakan 25% untuk metaspace, thread stack, code cache, dan direct buffer. Hindari -Xmx statis: nilainya tidak ikut berubah bila limit container diubah. Memasang setelan lewat JAVA_TOOL_OPTIONS membuatnya bisa ditimpa saat deploy tanpa build ulang.
Contoh di dunia backend
Tim toko online punya order-service (Spring Boot 4.1) yang di-deploy beberapa kali sehari. Sebelum dioptimasi, Dockerfile mereka memakai eclipse-temurin:25-jdk, COPY . ., dan menyalin fat jar utuh — image ~600 MB dan setiap push mengirim ulang ~90 MB. Setelah memakai Dockerfile multi-stage + layered jar di atas, ditambah .dockerignore, image runtime turun drastis dan push harian hanya mengirim layer application beberapa ratus KB.
Image dijalankan dengan limit memori dan variabel lingkungan yang menimpa konfigurasi:
bash
docker build -t toko/order-service:1.4.2 .
docker run -d --name order-service \
--memory=768m --cpus=1 \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://db.internal:5432/orders \
toko/order-service:1.4.2
docker logs -f order-serviceTag image memakai versi aplikasi (atau SHA commit), bukan latest, sehingga rollback cukup menjalankan tag sebelumnya. Log ditulis ke stdout agar dikumpulkan oleh platform, bukan ke file di dalam container.
Pertanyaan interview
1. Apa beda image dan container?
Image adalah template read-only yang tersusun dari layer; container adalah instance image yang sedang berjalan dengan layer tulis sendiri. Dari satu image kamu bisa menjalankan banyak container, dan perubahan di container hilang saat container dihapus kecuali disimpan di volume.
2. Kenapa memakai multi-stage build dan layered jar untuk Spring Boot?
Multi-stage memisahkan tool build (JDK, Maven, source) dari image runtime sehingga image kecil dan permukaan serangannya sempit. Layered jar memecah fat jar menjadi layer dependency dan layer aplikasi, sehingga perubahan kode hanya membangun ulang dan mengirim layer kecil — build dan deploy lebih cepat.
3. Bagaimana mengatur memori JVM di container?
JVM membaca limit cgroup, jadi gunakan -XX:MaxRAMPercentage (mis. 70–75) alih-alih -Xmx statis. Sisakan ruang untuk memori non-heap; bila heap memakai seluruh limit, proses bisa di-OOMKilled oleh kernel walau heap belum penuh.
Jebakan umum
- Menjalankan aplikasi sebagai
rootdi container — bila aplikasi dibobol, penyerang punya hak penuh di dalam container. TambahkanUSERnon-root. COPY . .sebelum mengunduh dependency — cache batal setiap perubahan kode dan build jadi lambat.- Memakai tag
latestuntuk deploy — kamu tidak tahu versi mana yang berjalan dan rollback jadi tebakan. ENTRYPOINT java -jar app.jar(bentuk shell) —javabukan PID 1, sinyalSIGTERMtidak sampai dan graceful shutdown gagal. Pakai bentuk array.- Lupa
.dockerignore—target/,.git/, bahkan file.envberisi secret ikut masuk build context. - Menyetel
-Xmxsama dengan limit memori container — memori non-heap membuat proses di-kill.
Ringkasan
- Container adalah proses terisolasi yang berbagi kernel host; image tersusun dari layer yang di-cache per instruksi.
- Dockerfile multi-stage: build dengan JDK + Maven, jalankan dengan
eclipse-temurin:25-jre. - Layered jar (
java -Djarmode=tools -jar app.jar extract --layers) memisahkan dependency dari kode agar build & push cepat; Buildpacks (./mvnw spring-boot:build-image) memberi hasil serupa tanpa Dockerfile. - Jalankan sebagai user non-root, pakai
ENTRYPOINTbentuk array, dan sertakan.dockerignore. - Atur heap dengan
-XX:MaxRAMPercentage, bukan-Xmxstatis.