Konten sudah siap di site/; sekarang kita menulis kotaknya. Tiga berkas lahir di bagian ini dan ketiganya saling mengunci: Dockerfile adalah resep pengemasan, nginx.conf menentukan bagaimana isi kotak itu disajikan, dan .dockerignore menjaga agar hanya bahan yang perlu yang ikut terkirim ke builder. Urutan penulisan kita mengikuti urutan eksekusinya kelak — build context disaring, Dockerfile dibaca, lalu baris demi barisnya membentuk layer-layer image. Setiap baris yang kita tulis di sini punya jejak yang bisa dibaca kembali: di output BuildKit pada Bagian 3 dan di header HTTP pada Bagian 4.
Dockerfile untuk Situs Statis #
Buat berkas Dockerfile di akar folder proyek dengan isi berikut:
# Image berbasis nginx resmi versi minor yang dipin.
FROM nginx:1.29-alpine
LABEL org.opencontainers.image.title="container-web-statis" \
org.opencontainers.image.description="Situs katalog produk statis yang disajikan nginx" \
org.opencontainers.image.source="https://example.com/container-web-statis" \
org.opencontainers.image.licenses="MIT"
# Konfigurasi server menimpa default.conf bawaan image.
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Konten situs disalin setelah konfigurasi agar layer mudah di-cache.
COPY site/ /usr/share/nginx/html
EXPOSE 80
# Validasi konfigurasi saat build: gagal di sini lebih baik daripada crash saat runtime.
RUN nginx -tMari kita telaah keenam instruksi tersebut satu per satu:
FROM nginx:1.29-alpine— image dasar resmi nginx, dipin pada minor 1.29 dengan varian Alpine 47.9MB. Kebiasaan memilih tag eksplisit ini warisan langsung Memahami Image Tag: taglatestbisa bergeser diam-diam, sedangkan1.29-alpineterkunci pada rilis 1.29 — dan kelak terbukti lewat headerServer: nginx/1.29.4di Bagian 4. Mengunci lebih dalam lagi dengan digest tetap tersedia lewat polaFROM image@sha256:…dari Image Digest.LABELdengan kunci OCI — empat pasang kunci-nilaiorg.opencontainers.image.*menempelkan metadata standar Open Containers pada image: judul, deskripsi, sumber, dan lisensi. Label tidak menambah layer filesystem — ia hidup di konfigurasi image, persis perilaku yang kita bedah pada VOLUME dan LABEL — tapi membuat image ini dikenali tool ekosistem: registry, scanner, dan catalog.COPY nginx.conf /etc/nginx/conf.d/default.conf— konfigurasi kita menimpadefault.confbawaan image di lokasi yang tepat: nginx memuat seluruhconf.d/*.confdari dalam blokhttppada konfigurasi utamanya. Tanpa penimpaan ini, container akan menyajikan halaman selamat datang bawaan nginx — bukan katalog Kopi Nusantara.COPY site/ /usr/share/nginx/html— seluruh isi folder konten Bagian 1 disalin ke direktori penyajian bawaan image. Perhatikan urutannya: konfigurasi disalin lebih dulu, konten belakangan — persis strategi cache dari Build Cache. Suntingansite/index.htmlkelak hanya meruntuhkan layer keempat ke bawah, sementara layerCOPY nginx.confbeserta semuanya di atasnya tetapCACHED.EXPOSE 80— catatan metadata bahwa container ini mendengarkan port 80. Ia tidak menerbitkan port apa pun ke host — tugas itu dipegang flag-pdan kunciports— sebagaimana kita tegaskan tuntas di EXPOSE dan Port.RUN nginx -t— jangkar keselamatan proyek ini: image nginx menyertakan binarynginx, dannginx -tmemvalidasi seluruh konfigurasi yang barusan kita timpa. Bilanginx.confkeliru, build gagal di sini — bukan saat container dinyalakan di depan pengunjung. Komentar pada barisnya mengatakan tegas: gagal saat build lebih baik daripada crash saat runtime.
Satu instruksi yang sengaja tidak kita tulis justru paling sering ditanyakan pemula: di mana CMD? Jawabannya: kita mewarisinya. Image nginx resmi membawa ENTRYPOINT bernama /docker-entrypoint.sh dan CMD berupa nginx -g daemon off; — skrip entrypoint itu pula yang menyiapkan konfigurasi bawaan setiap kali container menyala. Karena Dockerfile kita tidak menimpa keduanya, container hasil build tetap menjalankan nginx di latar depan sebagai PID 1, persis perilaku yang kita bedah pada CMD dan ENTRYPOINT. Menulis ulang CMD yang sama hanya menambah satu baris yang bisa salah; mewarisi yang benar adalah pilihan paling bersih.
Konfigurasi nginx Gaya conf.d #
Berkas kedua adalah nginx.conf — meski namanya begitu, isinya bukan konfigurasi utama melainkan satu blok server gaya conf.d yang ditempelkan menimpa default.conf bawaan image. Simpan di akar proyek:
# Konfigurasi server untuk situs statis (gaya conf.d).
# Berkas ini menimpa default.conf bawaan image nginx dan otomatis
# dimuat oleh blok http lewat directive include conf.d/*.conf.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Kompresi respons untuk tipe selain text/html (selalu dikompresi).
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
# Aset statis boleh di-cache panjang oleh browser.
location ~* \.(css|js|png|jpg|jpeg|svg|ico)$ {
expires 7d;
add_header Cache-Control public;
}
# index.html selalu divalidasi ulang agar pembaruan konten langsung terlihat.
location = /index.html {
add_header Cache-Control no-cache;
}
location / {
try_files $uri $uri/ =404;
}
}Mari kita telaah enam kelompok directive di dalamnya:
listen 80danserver_name _— blok server ini menerima koneksi pada port 80 untuk nama host apa pun; garis bawah adalah konvensi catch-all. Port yang didengarkan ini harus sejajar denganEXPOSE 80di Dockerfile dan sisi:80pada pemetaan port di Bagian 3 — tiga tempat, satu angka.rootdanindex— direktori penyajian/usr/share/nginx/htmldan berkas pembukaindex.html. Dua baris ini saling mengunci dengan Dockerfile:rootwajib sama dengan tujuanCOPY site/, danindexwajib sama dengan nama berkas Bagian 1. Satu huruf saja berbeda, dan yang muncul di layar pengunjung adalah404— bukan katalog.gzip on,gzip_types,gzip_min_length 256— kompresi menyala untuktext/css,application/javascript,application/json, danimage/svg+xml. Yang tidak tercantum bukan berarti lolos:text/htmlselalu ikut dikompresi ketikagzip on— ia tidak perlu — dan tidak boleh — ditulis digzip_types. Ambang 256 byte mencegah respons mungil justru membesar karena pembungkus kompresi.location ~* \.(css|js|png|jpg|jpeg|svg|ico)$— pola regex untuk berkas aset:expires 7dmenetapkan headerExpirestujuh hari ke depan, danadd_header Cache-Control publicmengizinkan cache bersama menyimpannya. Situs kita hari ini belum punya aset seperti itu — blok ini investasi untuk saatsite/kemudian hari berisi CSS dan gambar.location = /index.html— kebalikan dari aset: halaman utama justru tidak boleh dijamah cache lama.Cache-Control no-cacheberarti “boleh disimpan, tapi wajib divalidasi ulang ke server sebelum dipakai” — berbeda darino-storeyang melarang penyimpanan sama sekali. Dengan begitu pembaruan konten terlihat seketika, tanpa membayar biaya unduhan penuh setiap kunjungan.location /dengantry_files $uri $uri/ =404— penjaga gerbang terakhir: coba berkas persis namanya, coba sebagai folder, lalu menyerah dengan404yang tegas. Tanpa fallback=404, permintaan ke jalur yang tak ada bisa berakhir pada daftar isi direktori otomatis — perilaku yang tidak kita inginkan dari situs publik.
.dockerignore untuk Build Context #
Berkas ketiga menyeleksi apa yang boleh meninggalkan folder proyek menuju builder. Simpan sebagai .dockerignore:
# Berkas yang tidak perlu ikut dalam build context.
.git
.gitignore
README.md
bagian/
*.mdLima baris penyaring ini bekerja sebelum satu byte context pun terkirim, dan alasan pemakaiannya ada tiga:
- Context tetap ramping. Tanpa penyaring, riwayat Git, dokumentasi, dan materi penulisan chapter ikut diarsipkan dan dikirim ke builder pada setiap build — di mesin sungguhan,
.gitsaja bisa berukuran puluhan megabyte. Dengan penyaring, context proyek ini tinggal hitungan kilobyte, dan angka persisnya kita saksikan pada baristransferring contextdi Bagian 3. - Cache
COPYtetap awet. Checksum layer dihitung dari isi context yang terkirim; menyuntingREADME.mdatau menambah catatan pada*.mdlain tidak lagi meruntuhkan rantai cache — mekanisme invalidasi yang kita bedah pada .dockerignore dan Build Cache. - Image hanya berisi yang dibutuhkan. Folder
bagian/berisi materi penulisan course ini — tidak ada alasannya ikut menumpang di dalam image produksi. Penyaringan menjaga batas yang sama dengan pemisahansite/di Bagian 1: konten, resep, dan dokumentasi tidak saling mencemari.
Satu kejutan lama juga berlaku di sini: menyaring Dockerfile sendiri tidak membuat build gagal. Builder membaca definisi build lewat jalurnya tersendiri — langkah load build definition pada output Bagian 3 — terpisah dari pengarsipan context, sebagaimana kita saksikan pada .dockerignore. .dockerignore kita tidak menyaring Dockerfile; ia hanya menahan *.md, README.md, bagian/, dan riwayat Git — sementara nginx.conf, compose.yaml, dan site/ tetap berangkat sebagai bahan build.
Tiga berkas kini berdiri di akar proyek, dan resepnya tinggal dibakar. Bagian berikutnya menghidupkan seluruh tulisan kita: docker build merakit layer demi layer dengan output BuildKit yang kita baca baris demi baris, docker run menghidupkan container dengan flag-flag yang sudah akrab, lalu docker compose up memindahkan semuanya ke dalam satu berkas deklarasi. Ikuti terus — angka-angka pada Bagian 4 semuanya lahir dari keputusan yang kita ambil di bagian ini.
Referensi #
- Dockerfile Reference
— semantik resmi
FROM,LABEL,COPY,EXPOSE, danRUN. - Build Contexts
— aturan
.dockerignore, pola glob, dan perilaku pengarsipan context.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
compose.yaml
Unduh berkasname: container-web-statis
services:
web:
build: .
image: container-web-statis:1.0
container_name: web-statis
ports:
- "8080:80"
restart: unless-stopped
site/index.html
Unduh berkas<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Katalog produk Kopi Nusantara: Torabika, Gayo, dan Kintamani.">
<title>Katalog Produk — Kopi Nusantara</title>
<style>
/* Palet warna terpusat sebagai custom properties. */
:root {
--latar: #f7f4ef;
--teks: #2b2723;
--aksen: #6f4e37;
--kartu: #ffffff;
}
body {
font-family: Georgia, serif;
background: var(--latar);
color: var(--teks);
margin: 0;
line-height: 1.6;
}
header, footer { background: var(--aksen); color: #fff; text-align: center; padding: 1rem; }
h1 { margin: 0; font-size: 1.6rem; }
main { max-width: 680px; margin: 2rem auto; padding: 0 1rem; }
h2 { color: var(--aksen); margin: 0 0 1rem; }
.pengantar { margin: 0 0 1.5rem; }
section.produk { display: grid; gap: 1rem; }
.kartu {
background: var(--kartu);
border: 1px solid #e6dfd3;
border-radius: 8px;
padding: 1rem;
}
.kartu h3 { margin: 0 0 0.25rem; }
.harga { color: var(--aksen); font-weight: bold; margin: 0 0 0.25rem; }
.kartu p { margin: 0; }
</style>
</head>
<body>
<header>
<h1>Kopi Nusantara</h1>
<p>Kopi pilihan Indonesia</p>
</header>
<main>
<h2>Daftar Produk</h2>
<p class="pengantar">Tiga kopi favorit kami, disangrai segar setiap pekan.</p>
<section class="produk">
<article class="kartu">
<h3>Kopi Torabika</h3>
<p class="harga">Rp 28.000</p>
<p>Robusta dengan aroma kuat, favorit pelanggan.</p>
</article>
<article class="kartu">
<h3>Kopi Gayo</h3>
<p class="harga">Rp 45.000</p>
<p>Arabika dataran tinggi Aceh dengan aroma rempah.</p>
</article>
<article class="kartu">
<h3>Kopi Kintamani</h3>
<p class="harga">Rp 42.000</p>
<p>Arabika Bali dengan sentuhan rasa sitrus.</p>
</article>
</section>
</main>
<footer>
<p>© 2026 Kopi Nusantara — disajikan oleh nginx</p>
</footer>
</body>
</html>