Setiap proyek container dimulai dari pertanyaan yang sama: apa sebenarnya yang kita masukkan ke dalam image? Pada proyek ini jawabannya sederhana — satu berkas HTML — tetapi cara kita menyusunnya menentukan kelancaran tiga bagian berikutnya. Folder tempat berkas itu hidup akan menjadi jalur COPY di Dockerfile, ukurannya akan menjadi Content-Length pada respons HTTP, dan waktu pembuatannya akan tercetak sebagai Last-Modified serta ETag. Karena itu kita mulai dari konten, bukan dari container: bangun dulu barangnya, baru kotaknya.
Membuat Struktur Folder #
Semua berkas proyek hidup dalam satu folder bernama container-web-statis. Buat folder itu beserta subfolder site/ dalam satu perintah:
$ mkdir -p container-web-statis/site
$ cd container-web-statisFlag -p membuat seluruh tingkat jalur sekaligus — folder induk dan anaknya — tanpa protes bila sebagian sudah ada. Perintah ini tidak mencetak apa pun; keberhasilannya kita buktikan dengan melihat struktur yang baru lahir:
container-web-statis/
└── site/Subfolder site/ sengaja dipisah dari berkas-berkas konfigurasi yang menyusul di Bagian 2. Pemisahan ini bukan gaya-gayaan: kelak Dockerfile menyalin seluruh isi site/ ke direktori penyajian nginx, sehingga batas antara “konten yang tampil di web” dan “resep cara menyajikannya” tergambar jelas sejak struktur foldernya.
Menulis index.html #
Satu-satunya berkas konten proyek ini adalah site/index.html berikut. Simpan utuh — kita bedah setiap keputusannya setelah ini:
<!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>Struktur folder kini berbentuk:
container-web-statis/
└── site/
└── index.htmlMari kita telaah enam keputusan di balik berkas ini:
lang="id"dancharset="UTF-8"— bahasa dan encoding dideklarasikan di dua tempat sekaligus: atributlanguntuk pembaca layar dan mesin pencari,<meta charset>agar tanda hubung em pada judul tersimpan dan terkirim utuh. Encoding yang sama akan kita andalkan saat membaca byte-byte responsnya lewatcurldi Bagian 4.- Seluruh gaya hidup di satu
<style>— tidak ada berkas CSS atau JavaScript eksternal, sehingga satu kunjungan ke halaman ini cukup satu permintaan HTTP. Konsekuensinya positif untuk proyek container: tidak ada lokasi aset yang harus dikonfigurasi, tidak ada404beruntun karena path CSS salah, dan kompresi gzip nanti mengenyam seluruh halaman dalam satu tarikan. - Custom properties pada
:root— empat variabel warna (--latar,--teks,--aksen,--kartu) menjadi palet terpusat; ganti satu nilai dan seluruh halaman ikut berubah. Pola yang sama — konfigurasi terpusat di satu tempat — akan kita lihat lagi padanginx.confdancompose.yaml. - Semantik
header,main,footer— struktur dokumen membaca seperti kerangka toko: papan nama di atas, etalase di tengah, kop surat di bawah. Browser, pembaca layar, dan crawler sama-sama memahami peta ini tanpa satu baris JavaScript pun. section.produkberisiarticle.kartu— tiga kartu kopi disusun grid berjarak rapi; tiap kartu adalaharticlemandiri dengan judul, harga, dan deskripsi satu kalimat. Menambah produk keempat cukup menyalin satu blok kartu.- Footer bertanda tangan — baris penutup
© 2026 Kopi Nusantara — disajikan oleh nginxsengaja menyebut nama servernya. Kalimat ini adalah janji yang kita lunasi di Bagian 4: bukti bahwa halaman yang terbuka di browser memang disajikan oleh nginx dari dalam container.
Statis versus Dinamis #
Kata “statis” pada judul proyek bukan berarti membosankan — ia adalah keputusan arsitektur dengan konsekuensi yang menjalar sampai image dasar yang kita pilih. Tabel berikut memetakan posisi proyek ini:
| Aspek | Situs Statis (proyek ini) | Situs Dinamis |
|---|---|---|
| Konten | Berkas HTML siap saji — sama untuk semua pengunjung | Dirangkai runtime untuk setiap permintaan |
| Runtime | Cukup web server — nginx menyajikan berkas | Bahasa server (Node, Go, Python) mengeksekusi kode |
| Image dasar | nginx:1.29-alpine, 47.9MB | Image runtime aplikasi plus kode, jauh lebih besar |
| State | Stateless murni — container boleh dibuang kapan saja | Sesi, database, dan volume persisten ikut bermain |
Bagian kiri tabel menjelaskan kenapa proyek ini bisa selesai dalam empat bagian: tanpa database, tanpa migrasi, tanpa healthcheck. Bagian kanannya adalah dunia Proyek: Aplikasi Go dan PostgreSQL pada urutan berikutnya — saat tiba saatnya, seluruh kebiasaan yang kita bangun di sini tetap terpakai.
Pratinjau di Browser Lokal #
Sebelum menyentuh Docker, pastikan halaman ini memang enak dilihat. Di Linux, perintah pembuka browser default:
$ xdg-open site/index.htmlPerintah ini tidak mencetak output — tugasnya satu: membuka site/index.html di browser. Halaman katalog dengan tiga kartu kopi akan tampil, dan address bar menunjukkan alamat berbentuk file:///.... Di situ letak nuansa pentingnya: file:// berarti browser membaca berkas langsung dari disk, tanpa ada server di antaranya.
Akibatnya, sejumlah perilaku yang menjadi pokok bahasan proyek ini belum terjadi pada pratinjau lokal: tidak ada header Server, tidak ada Content-Length, tidak ada ETag, tidak ada Cache-Control, dan tentu tidak ada kompresi gzip — semuanya adalah perilaku server, bukan perilaku berkas. Barulah ketika konten ini disajikan nginx dari dalam container pada Bagian 3 dan diuji dengan curl pada Bagian 4, kesepuluh baris respons itu hidup. Pratinjau file:// memeriksa tampilan; pengujian HTTP memeriksa penyajian — keduanya sama penting, tapi berbeda tempat kerjanya.
Konten sudah berdiri di tempatnya, dan struktur folder menunggu para pendampingnya. Dua pertanyaan besar tinggal menjawab: dengan resep apa satu folder HTML ini dikemas menjadi image, dan dengan konfigurasi apa nginx menyajikannya? Keduanya dijawab sekaligus di Bagian 2: Membuat Dockerfile — di sana kita menulis Dockerfile, nginx.conf, dan .dockerignore sampai tuntas, termasuk satu trik location yang baru akan terbukti efeknya di Bagian 4.
Referensi #
- nginx di Docker Hub — image resmi yang akan menjadi basis Dockerfile di Bagian 2.
- Dokumentasi nginx — rujukan directive penyajian berkas statis yang dipakai proyek ini.
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>