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:

bash
$ mkdir -p container-web-statis/site
$ cd container-web-statis

Flag -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:

text
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.

Satu-satunya berkas konten proyek ini adalah site/index.html berikut. Simpan utuh — kita bedah setiap keputusannya setelah ini:

html
<!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>&copy; 2026 Kopi Nusantara — disajikan oleh nginx</p>
  </footer>
</body>
</html>

Struktur folder kini berbentuk:

text
container-web-statis/
└── site/
    └── index.html

Mari kita telaah enam keputusan di balik berkas ini:

  1. lang="id" dan charset="UTF-8" — bahasa dan encoding dideklarasikan di dua tempat sekaligus: atribut lang untuk 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 lewat curl di Bagian 4.
  2. 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 ada 404 beruntun karena path CSS salah, dan kompresi gzip nanti mengenyam seluruh halaman dalam satu tarikan.
  3. 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 pada nginx.conf dan compose.yaml.
  4. 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.
  5. section.produk berisi article.kartu — tiga kartu kopi disusun grid berjarak rapi; tiap kartu adalah article mandiri dengan judul, harga, dan deskripsi satu kalimat. Menambah produk keempat cukup menyalin satu blok kartu.
  6. Footer bertanda tangan — baris penutup &copy; 2026 Kopi Nusantara — disajikan oleh nginx sengaja 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:

AspekSitus Statis (proyek ini)Situs Dinamis
KontenBerkas HTML siap saji — sama untuk semua pengunjungDirangkai runtime untuk setiap permintaan
RuntimeCukup web server — nginx menyajikan berkasBahasa server (Node, Go, Python) mengeksekusi kode
Image dasarnginx:1.29-alpine, 47.9MBImage runtime aplikasi plus kode, jauh lebih besar
StateStateless murni — container boleh dibuang kapan sajaSesi, 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:

bash
$ xdg-open site/index.html

Perintah 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

compose.yaml

Unduh berkas
yaml
name: 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
html
<!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>&copy; 2026 Kopi Nusantara — disajikan oleh nginx</p>
  </footer>
</body>
</html>