Selama ini kita menyiapkan mesin target sebelum aplikasi tiba: pasang JDK versi tepat, atur environment variable, salin runtime jlink. Setiap mesin adalah kemungkinan baru untuk perbedaan kecil yang berakibat besar — masalah klasik “di mesin saya jalan”. Container menyelesaikannya dengan membalik arah: aplikasi membawa lingkungannya sendiri dalam bentuk paket standar yang bisa dijalankan identik di laptop, server fisik, maupun cluster cloud. Docker adalah implementasi container yang paling umum, dan bagi deployment Java modern memahaminya bukan lagi opsional. Chapter ini membahas tiga konsep intinya — image, container, dan Dockerfile — lalu membongkar mekanisme cache yang membuat build image menjadi cepat.
Image, Container, dan Registry #
Tiga istilah ini saling bertaut dan sering tertukar:
| Istilah | Analogi | Sifat |
|---|---|---|
| Image | cetakan/program terpasang | immutable, berlapis, diberi tag |
| Container | proses yang berjalan dari image | bisa dinyalakan/dimatikan, ephemeral |
| Registry | gudang distribusi image | Docker Hub, registry privat |
Satu image bisa menurunkan banyak container yang berjalan serentak — seperti satu berkas toko-fat.jar dijalankan di sepuluh mesin. Image dibangun dari Dockerfile: resep berisi instruksi yang dieksekusi berurutan dari atas ke bawah. Untuk aplikasi Java kita, resep termuda biasanya hanya beberapa baris:
FROM eclipse-temurin:25-jre
COPY target/toko-fat.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]Baris FROM mengambil image dasar berisi JRE 25 resmi Temurin, COPY memasukkan artefak aplikasi, EXPOSE mendokumentasikan port yang dipakai, dan ENTRYPOINT menentukan perintah yang dijalankan setiap container dinyalakan. Membangun dan menjalankannya memakai dua perintah inti:
docker build -t kasirapp:1.0.0 .
docker run --rm -p 8080:8080 kasirapp:1.0.0Flag -t memberi nama dan tag pada image, --rm membuang container setelah berhenti, dan -p menjembatani port mesin ke port container.
Lapisan Image dan Cache #
Setiap instruksi Dockerfile menghasilkan satu layer yang ditumpuk di atas layer sebelumnya. Builder Docker menyimpan cache layer ini — dan aturan mainnya menentukan cepat atau lambatnya workflow harian: begitu satu layer berubah, seluruh layer di atasnya harus dibangun ulang. Program berikut memodelkan mekanisme itu dengan hash berantai:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Model lapisan image: layer ID berantai + cache seperti builder Docker. */
public class LapisanImage {
/** Satu instruksi Dockerfile beserta "konten" berkas yang dibawanya. */
record Instruksi(String teks, String konten) {}
/** ID lapisan = hash(parent + instruksi + konten), seperti cache Docker. */
static String idLapisan(String idOrtu, Instruksi i) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
String bahan = (idOrtu == null ? "-" : idOrtu) + "|" + i.teks() + "|" + i.konten();
byte[] cerna = md.digest(bahan.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (int j = 0; j < 6; j++) {
hex.append(String.format("%02x", cerna[j]));
}
return hex.toString();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/** Jalankan "docker build": HIT bila ID lapisan pernah dibangun sebelumnya. */
static void build(String judul, List<Instruksi> dockerfile, Map<String, Boolean> cache) {
System.out.println("== " + judul + " ==");
String idOrtu = null;
int hit = 0;
for (Instruksi i : dockerfile) {
String id = idLapisan(idOrtu, i);
boolean tercache = cache.containsKey(id);
if (tercache) {
hit++;
} else {
cache.put(id, true);
}
System.out.printf(" %-44s %s %s%n", i.teks(), id, tercache ? "HIT" : "MISS");
idOrtu = id;
}
System.out.printf(" lapisan terpakai ulang: %d/%d%n%n", hit, dockerfile.size());
}
public static void main(String[] args) {
List<Instruksi> dockerfile = List.of(
new Instruksi("FROM eclipse-temurin:25-jre", "image dasar"),
new Instruksi("COPY libs/util.jar /app/libs/util.jar", "util.jar v1.0.0"),
new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v1.0.0"),
new Instruksi("ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]", "-"));
Map<String, Boolean> cache = new HashMap<>();
build("Build 1: cache masih kosong", dockerfile, cache);
build("Build 2: tanpa perubahan apa pun", dockerfile, cache);
List<Instruksi> kodeBaru = new ArrayList<>(dockerfile);
kodeBaru.set(2, new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v1.0.1"));
build("Build 3: hanya kode aplikasi berubah", kodeBaru, cache);
// Urutan instruksi memengaruhi jumlah lapisan yang bisa dipakai ulang.
List<Instruksi> srcDulu = List.of(
new Instruksi("FROM eclipse-temurin:25-jre", "image dasar"),
new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v2.0.0"),
new Instruksi("COPY libs/util.jar /app/libs/util.jar", "util.jar v1.0.0"),
new Instruksi("ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]", "-"));
build("Build 4: kode disalin lebih dulu (urutan kurang tepat)", srcDulu, cache);
List<Instruksi> depDulu = List.of(
new Instruksi("FROM eclipse-temurin:25-jre", "image dasar"),
new Instruksi("COPY libs/util.jar /app/libs/util.jar", "util.jar v1.0.0"),
new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v2.0.0"),
new Instruksi("ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]", "-"));
build("Build 5: dependency dulu, kode paling akhir (urutan tepat)", depDulu, cache);
}
}Output yang Dihasilkan:
== Build 1: cache masih kosong ==
FROM eclipse-temurin:25-jre 774a00eead9f MISS
COPY libs/util.jar /app/libs/util.jar 63af376ebaf9 MISS
COPY target/toko-app.jar /app/app.jar ba47c22b194d MISS
ENTRYPOINT ["java","-jar","/app/app.jar"] c84f17fd19ec MISS
lapisan terpakai ulang: 0/4
== Build 2: tanpa perubahan apa pun ==
FROM eclipse-temurin:25-jre 774a00eead9f HIT
COPY libs/util.jar /app/libs/util.jar 63af376ebaf9 HIT
COPY target/toko-app.jar /app/app.jar ba47c22b194d HIT
ENTRYPOINT ["java","-jar","/app/app.jar"] c84f17fd19ec HIT
lapisan terpakai ulang: 4/4
== Build 3: hanya kode aplikasi berubah ==
FROM eclipse-temurin:25-jre 774a00eead9f HIT
COPY libs/util.jar /app/libs/util.jar 63af376ebaf9 HIT
COPY target/toko-app.jar /app/app.jar 5137bf832e75 MISS
ENTRYPOINT ["java","-jar","/app/app.jar"] 3248684a0eef MISS
lapisan terpakai ulang: 2/4
== Build 4: kode disalin lebih dulu (urutan kurang tepat) ==
FROM eclipse-temurin:25-jre 774a00eead9f HIT
COPY target/toko-app.jar /app/app.jar e5e2bdf07aca MISS
COPY libs/util.jar /app/libs/util.jar 0be865d04acf MISS
ENTRYPOINT ["java","-jar","/app/app.jar"] cb40800cac80 MISS
lapisan terpakai ulang: 1/4
== Build 5: dependency dulu, kode paling akhir (urutan tepat) ==
FROM eclipse-temurin:25-jre 774a00eead9f HIT
COPY libs/util.jar /app/libs/util.jar 63af376ebaf9 HIT
COPY target/toko-app.jar /app/app.jar 5b41d316b957 MISS
ENTRYPOINT ["java","-jar","/app/app.jar"] cbb7ddac0c04 MISS
lapisan terpakai ulang: 2/4Mari kita baca hasilnya per skenario:
idLapisan(idOrtu, i): ID sebuah layer dihitung dari hash gabungan ID layer ortu + teks instruksi + konten berkas. Karena ID ortu ikut dihitung, rantai ini menular: satu layer yang berubah membuat seluruh keturunannya berganti ID.- Build 1 dan 2: build pertama mengisi cache (semua
MISS), build kedua tanpa perubahan menemukan keempat ID yang sama — empatHIT, tidak ada yang diunduh ulang. - Build 3: hanya
toko-app.jaryang berubah (v1.0.0 → v1.0.1). Dua layer teratasMISSmeskiutil.jartidak berubah — itulah penularan ID ortu di barisidOrtu = id. - Build 4 vs 5: dua urutan salinan yang sama isinya, hasilnya berbeda. Menyalin kode aplikasi lebih dulu membuat
util.jarikut dibangun ulang (1/4 terpakai); menempatkan dependency di atas dan kode paling bawah menjaga dua layer pertama tetapHIT(2/4). Prinsip praktisnya: yang paling sering berubah ditaruh paling akhir.
Perintah Pokok docker CLI #
| Perintah | Fungsi |
|---|---|
docker build | membangun image dari Dockerfile |
docker run | menyalakan container dari image |
docker ps | mendaftar container yang berjalan |
docker logs | membaca keluaran stdout/stderr container |
docker exec | menjalankan perintah di dalam container yang hidup |
docker stop / docker rm | menghentikan / membuang container |
Karena container bersifat ephemeral, log aplikasi Java dibaca lewat docker logs, bukan berkas di dalam container — implikasi besar bagi cara kita mendesain logging yang akan kita bahas di chapter tersendiri.
Konsep dasar ini cukup untuk memahami peta. Chapter berikutnya masuk ke wilayah praktik khusus Java: menulis Dockerfile yang ramping, memahami perilaku JVM di dalam batas memori container, dan memilih base image yang tepat.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
LapisanImage.java
Unduh berkasimport java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Model lapisan image: layer ID berantai + cache seperti builder Docker. */
public class LapisanImage {
/** Satu instruksi Dockerfile beserta "konten" berkas yang dibawanya. */
record Instruksi(String teks, String konten) {}
/** ID lapisan = hash(parent + instruksi + konten), seperti cache Docker. */
static String idLapisan(String idOrtu, Instruksi i) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
String bahan = (idOrtu == null ? "-" : idOrtu) + "|" + i.teks() + "|" + i.konten();
byte[] cerna = md.digest(bahan.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (int j = 0; j < 6; j++) {
hex.append(String.format("%02x", cerna[j]));
}
return hex.toString();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/** Jalankan "docker build": HIT bila ID lapisan pernah dibangun sebelumnya. */
static void build(String judul, List<Instruksi> dockerfile, Map<String, Boolean> cache) {
System.out.println("== " + judul + " ==");
String idOrtu = null;
int hit = 0;
for (Instruksi i : dockerfile) {
String id = idLapisan(idOrtu, i);
boolean tercache = cache.containsKey(id);
if (tercache) {
hit++;
} else {
cache.put(id, true);
}
System.out.printf(" %-44s %s %s%n", i.teks(), id, tercache ? "HIT" : "MISS");
idOrtu = id;
}
System.out.printf(" lapisan terpakai ulang: %d/%d%n%n", hit, dockerfile.size());
}
public static void main(String[] args) {
List<Instruksi> dockerfile = List.of(
new Instruksi("FROM eclipse-temurin:25-jre", "image dasar"),
new Instruksi("COPY libs/util.jar /app/libs/util.jar", "util.jar v1.0.0"),
new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v1.0.0"),
new Instruksi("ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]", "-"));
Map<String, Boolean> cache = new HashMap<>();
build("Build 1: cache masih kosong", dockerfile, cache);
build("Build 2: tanpa perubahan apa pun", dockerfile, cache);
List<Instruksi> kodeBaru = new ArrayList<>(dockerfile);
kodeBaru.set(2, new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v1.0.1"));
build("Build 3: hanya kode aplikasi berubah", kodeBaru, cache);
// Urutan instruksi memengaruhi jumlah lapisan yang bisa dipakai ulang.
List<Instruksi> srcDulu = List.of(
new Instruksi("FROM eclipse-temurin:25-jre", "image dasar"),
new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v2.0.0"),
new Instruksi("COPY libs/util.jar /app/libs/util.jar", "util.jar v1.0.0"),
new Instruksi("ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]", "-"));
build("Build 4: kode disalin lebih dulu (urutan kurang tepat)", srcDulu, cache);
List<Instruksi> depDulu = List.of(
new Instruksi("FROM eclipse-temurin:25-jre", "image dasar"),
new Instruksi("COPY libs/util.jar /app/libs/util.jar", "util.jar v1.0.0"),
new Instruksi("COPY target/toko-app.jar /app/app.jar", "toko-app.jar v2.0.0"),
new Instruksi("ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]", "-"));
build("Build 5: dependency dulu, kode paling akhir (urutan tepat)", depDulu, cache);
}
}