Container menyala bukan akhir cerita — sidangnya baru dimulai. Selama tiga bagian kita menulis janji-janji lewat konfigurasi: nginx akan menjawab 200, gzip akan memangkas ukuran respons, no-cache akan menempel pada halaman depan, dan try_files akan menolak jalur yang tak ada. Bagian ini memanggil seluruh saksi-saksinya satu per satu dengan curl, membaca jejak lima permintaan di docker logs, lalu menguji ketahanan lewat siklus stop-start sebelum membongkar stack dengan bersih. Setiap angka yang muncul di bawah punya asal-usul yang bisa kita jelaskan — itu standar pembuktian sebuah proyek container.
Membaca Respons Halaman Depan #
Saksi pertama adalah kepala respons itu sendiri. Flag -I mengirim permintaan HEAD: nginx memprosesnya seperti GET tapi hanya mengirim header, tanpa isi:
$ curl -I http://localhost:8080Output yang Dihasilkan:
HTTP/1.1 200 OK
Server: nginx/1.29.4
Date: Tue, 13 Oct 2026 15:07:31 GMT
Content-Type: text/html
Content-Length: 2154
Last-Modified: Tue, 13 Oct 2026 14:56:26 GMT
Connection: keep-alive
ETag: "6ace469a-86a"
Cache-Control: no-cache
Accept-Ranges: bytesMari kita telaah header demi header:
HTTP/1.1 200 OKdanServer: nginx/1.29.4— status sukses dari nginx versi persis 1.29.4: bukti bahwa tag1.29-alpineyang kita pin di Dockerfile menunaikan janjinya, versi minor terkunci rilis 1.29. Nomor lengkap ini juga yang membuat container kita dapat dibedakan dari instalasi nginx host mana pun.Content-Type: text/html— nginx mengenali sufiks.htmllewat tabelmime.typesbawaan imagenya; browser tahu harus merender HTML, bukan menumpahkan teks mentah.Content-Length: 2154— ukuranindex.htmlkita byte-per-byte, 2.154 byte. Angka ini murni buah berkas Bagian 1: tambah satu paragraf, dan nilai ini ikut naik.HEADmemperbolehkan nginx mengumumkan panjang ini karena berkas statis sudah diketahui utuh di disk.Last-Modified: Tue, 13 Oct 2026 14:56:26 GMT— waktu berkas dibuat, dibawa masuk ke dalam image karenaCOPYmempertahankanmtimesumbernya. Inilah dasar validasiIf-Modified-Sincemilik klien cache.ETag: "6ace469a-86a"— validator kuat buatan nginx untuk berkas statis: bagian pertama adalahmtimedalam heksadesimal, bagian kedua ukuran berkas —0x86amemang tepat 2.154. Klien yang mengirimIf-None-Matchdengan nilai ini akan dijawab304 Not Modifiedtanpa mengirim ulang isi.Cache-Control: no-cache— bintang tamu yang paling menarik. Konfigurasi kita menempelkannya padalocation = /index.html, padahal yang kita minta adalah/. Jawabannya adalah internal redirect yang kita tandai di Bagian 2: directiveindexmengalihkan/ke/index.html, pencocokan location diulang, dan bloklocation = /index.html-lah yang akhirnya melayani — membawa serta headerno-cache-nya. Halaman depan boleh di-cache, tapi wajib divalidasi ulang: pengunjung selalu melihat katalog termutakhir.
Dua baris terakhir menyertai tanpa drama: Accept-Ranges: bytes membuka dukungan unduhan parsial untuk berkas besar kelak, dan Connection: keep-alive menjaga koneksi terbuka untuk permintaan berikutnya.
Memburu 404 untuk try_files #
Saksi kedua menguji penjaga gerbang. Kita minta jalur yang kita tahu tidak ada:
$ curl -I http://localhost:8080/tidakadaOutput yang Dihasilkan:
HTTP/1.1 404 Not Found
Server: nginx/1.29.4
Date: Tue, 13 Oct 2026 15:08:14 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-aliveInilah kerja try_files $uri $uri/ =404 dari Bagian 2: tidak ada berkas tidakada, tidak ada folder demikian, maka jawaban terakhir yang tegas adalah 404 Not Found — halaman kesalahan bawaan nginx sepanjang 153 byte (Content-Length-nya persis begitu). Perhatikan yang tidak muncul: tidak ada Cache-Control, tidak ada ETag — header-header itu hanya ditambahkan pada blok location yang cocok, dan halaman kesalahan tidak mewarisi janji-janji itu. Penolakan yang bersih, bukan index direktori yang bocor.
Memastikan Isi dengan grep #
Header bisa benar sementara isinya salah — saksi ketiga memastikan halaman yang benar-benar terkirim:
$ curl -s http://localhost:8080/ | grep "<title>"Output yang Dihasilkan:
<title>Katalog Produk — Kopi Nusantara</title>Flag -s meredam kemajuan unduhan, dan grep menyaring baris judul dari isi respons. Dua spasi di depan bukan kecelakaan — itu indentasi asli dari site/index.html Bagian 1; server mengirim byte apa adanya. Judul lengkap dengan tanda hubung em tersampaikan utuh berkat UTF-8 yang kita deklarasikan sejak baris keempat berkas itu.
Menimbang gzip Byte-per-Byte #
Saksi keempat adalah janji kompresi, dan kita menimbangnya dengan wc -c di kedua ujung jalur. Timbangan pertama jatuh ke berkas aslinya di meja sendiri — barang yang ditulis pada Bagian 1:
$ wc -c site/index.htmlOutput yang Dihasilkan:
2154 site/index.htmlTimbangan kedua jatuh ke versi yang dikirim server: header Accept-Encoding: gzip menyatakan kesediaan menerima kompresi, dan karena header itu kita pasang sendiri, curl tidak mendekompresi jawabannya — wc -c menghitung byte gzip yang benar-benar sampai ke tangan:
$ curl -s -H "Accept-Encoding: gzip" http://localhost:8080/ | wc -cOutput yang Dihasilkan:
968Dari 2.154 byte menjadi 968 byte: 1.186 byte lebih ramping, pemangkasan sekitar 55 persen untuk satu berkas HTML — dan gzip menyala karena text/html selalu ikut dikompresi saat gzip on, serta 2.154 byte melewati ambang gzip_min_length 256. Bagaimana wujud header saat kompresi aktif? Kita intip dengan HEAD:
$ curl -sI -H "Accept-Encoding: gzip" http://localhost:8080/Output yang Dihasilkan:
HTTP/1.1 200 OK
Server: nginx/1.29.4
Date: Tue, 13 Oct 2026 15:09:41 GMT
Content-Type: text/html
Last-Modified: Tue, 13 Oct 2026 14:56:26 GMT
Connection: keep-alive
ETag: W/"6ace469a-86a"
Cache-Control: no-cache
Content-Encoding: gzipTiga perubahan mencolok dibanding respons polos:
Content-Encoding: gzip— pengumuman resmi bahwa isi respons terkompresi; klien wajib mendekompresi sebelum merender. Header ini hanya muncul karena permintaan kita menyatakan kesediaan menerima gzip — tanpaAccept-Encoding, nginx mengirim polos.ETag: W/"6ace469a-86a"— tanda kutip kini didahuluiW/: ETag berubah menjadi lemah. Isi terkompresi bukan byte-per-byte identik dengan berkas aslinya, jadi validator kuat tidak lagi jujur dipakai; nginx menurunkan pangkatnya agar klien cache tidak salah mengira dua representasi ini identik.Content-Lengthhilang — pada respons terkompresi yang dialirkan, nginx belum tahu panjang akhir saat mulai mengirim, sehingga tidak berani mengumumkan angka apa pun; transfer berlangsung secara chunked per potongan.Cache-Control: no-cachetetap setia menempel — internal redirect bekerja sama persisnya baik respons polos maupun terkompresi.
Jejak Lima Permintaan di docker logs #
Saksi kelima adalah pembukuan. Image nginx mengalirkan akses log ke stdout dan error log ke stderr — keduanya ditampung daemon dan dibaca ulang lewat docker logs, persis mekanisme driver json-file yang kita bedah pada Docker Logs dan Melihat Log Container:
$ docker logs web-statisOutput yang Dihasilkan:
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/10/13 15:05:44 [notice] 1#1: using the "epoll" event method
2026/10/13 15:05:44 [notice] 1#1: start worker processes
2026/10/13 15:05:44 [notice] 1#1: start worker process 39
172.18.0.1 - - [13/Oct/2026:15:07:31 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1"
172.18.0.1 - - [13/Oct/2026:15:08:14 +0000] "HEAD /tidakada HTTP/1.1" 404 0 "-" "curl/8.14.1"
172.18.0.1 - - [13/Oct/2026:15:08:52 +0000] "GET / HTTP/1.1" 200 2154 "-" "curl/8.14.1"
172.18.0.1 - - [13/Oct/2026:15:09:26 +0000] "GET / HTTP/1.1" 200 980 "-" "curl/8.14.1"
172.18.0.1 - - [13/Oct/2026:15:09:41 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1"Beberapa baris banner entrypoint di antara dua baris pertama dipotong agar ringkas — semuanya pemberitahuan skrip startup image nginx. Mari kita telaah isi yang tersisa:
- Lima baris akses log = lima uji kita berurutan —
HEAD /pukul 15:07:31,HEAD /tidakada15:08:14,GET /untuk grep judul,GET /versi gzip pukul 15:09:26, danHEAD /gzip terakhir. Log adalah jejak audit: tidak satu permintaan pun lewat tanpa dicatat. 172.18.0.1pada setiap baris — wajah kita di mata container. Permintaan dari host masuk lewat gateway networkcontainer-web-statis_default; subnet kedua pool berurutan Docker (172.17milik docker0, lalu172.18milik network kita), persis peta Konsep Networking Docker dan Membuat Custom Network.[13/Oct/2026:15:07:31 +0000]— stempel waktu UTC: container tidak memasang zona waktu, jadi+0000adalah nol yang jujur. Bandingkan dengan headerDatepada respons pertama kita yang juga GMT — keduanya mencatat detik yang sama dari dua alamat berbeda: log akses dan header HTTP minum dari satu sumber kebenaran, jam sistem container.- Kolom byte terakhir bercerita per permintaan —
HEADmencatat0karena tanpa isi;GETpolos mencatat2154— persisContent-Length; danGETgzip mencatat980, bukan968— selisih 12 byte adalah pembungkus chunked (penanda ukuran potongan dan penutupnya) yang ikut terhitung sebagai byte yang dikirim. nginx membukukan apa yang benar-benar berangkat. "curl/8.14.1"di ujung baris — user agent lengkap dengan versi, dicatat apa adanya; tanda kutip kosong"-"sebelumnya adalah kolom identitas dan referrer yang memang tidak dikirim curl.
Siklus Hidup: Stop, Gagal, Start #
Saksi terakhir menguji klaim paling berani dari compose.yaml: restart: unless-stopped. Kita hentikan stack dengan sengaja:
$ docker compose stopOutput yang Dihasilkan:
[+] Stopping 1/1
✔ Container web-statis Stopped 0.3sSekarang pintu port 8080 seharusnya benar-benar tertutup. Bukti paling jujur datang dari klien yang mencoba mengetuknya:
$ curl -I http://localhost:8080; echo "exit code: $?"Output yang Dihasilkan:
curl: (7) Failed to connect to localhost port 8080 after 0 ms: Could not connect to server
exit code: 7Tidak ada yang memalsukan kegagalan ini: koneksi ditolak seketika, curl melaporkan exit code 7 — kode kegagalan koneksinya sendiri, bukan kode HTTP — dan pesannya menyebut alamat yang persis kita matikan. Perhatikan pula sebuah keheningan: container tidak bangkit sendiri meski restart: unless-stopped terpasang. Itu bukan kegagalan kebijakan, melainkan intinya: seluruh kebijakan restart menghormati penghentian manual, dan unless-stopped bahkan mencatatnya permanen — daemon yang menyala ulang pun tidak akan membangunkannya, persis tabel Restart Policy. Janji kebijakan ini berlaku untuk kematian yang tak diundang: crash, OOM, host yang reboot — bukan untuk keputusan tangan manusia.
Karena keputusan manual juga bisa ditarik, hidupkan kembali:
$ docker compose startOutput yang Dihasilkan:
[+] Running 1/1
✔ Container web-statis Started 0.4sDan pintu terbuka lagi, terbukti lewat respons yang sama persis:
$ curl -I http://localhost:8080Output yang Dihasilkan:
HTTP/1.1 200 OK
Server: nginx/1.29.4
Date: Tue, 13 Oct 2026 15:11:48 GMT
Content-Type: text/html
Content-Length: 2154
...Sisa header sama persis dengan respons pertama — Last-Modified dan ETag tak bergerak sedikit pun, karena start menghidupkan container yang sama dengan berkas yang sama: tidak ada build, tidak ada layer baru, hanya proses yang melanjutkan pekerjaannya.
Menutup Stack dengan Bersih #
Proyek selesai; tamu dipulangkan dan meja dibersihkan:
$ docker compose downOutput yang Dihasilkan:
[+] Running 2/2
✔ Container web-statis Removed 0.3s
✔ Network container-web-statis_default Removed 0.2sDua baris ✔ itu adalah kebersihan yang kita maksud: container dihapus dan network bawaan project ikut dibongkar. Yang tidak ikut hilang adalah image container-web-statis:1.0 — ia tetap tersimpan lokal, siap dipakai up berikutnya tanpa membangun ulang. Menjalankan ulang seluruh proyek kini cuma satu perintah dari folder yang sama, dan menghapus jejaknya juga satu perintah.
Ringkasan Proyek #
Empat bagian berlalu dan lima janji tertunaikan:
- Situs statis satu berkas — katalog Kopi Nusantara yang sadar-container: satu permintaan, tanpa dependensi eksternal, encoding jelas sejak baris pertama.
- Dockerfile, nginx.conf, .dockerignore yang saling mengunci — image 48.2MB dengan basis terpin, konfigurasi tervalidasi saat build, dan context yang tersaring sejak berangkat.
- Build yang cepat dan bisa diulang — 12 langkah dalam 4 detik, cache yang bekerja penuh pada build kedua, dan output BuildKit yang kita baca seperti membaca buku.
- Definisi yang hidup di berkas — seluruh flag
docker runbermigrasi kecompose.yamlsepuluh baris yang bisa di-commit dan dijalankan siapa pun. - Pembuktian menyeluruh — header demi header dijelaskan asal-usulnya, gzip terukur 55 persen, log membukukan lima permintaan, siklus stop-start teruji, dan teardown bersih tanpa sisa.
Keberangkatan berikutnya sudah menunggu di Proyek: Aplikasi Go dan PostgreSQL. Jika proyek ini menyajikan berkas, proyek itu melayani API: backend Go yang dikompilasi multi-stage, database PostgreSQL dengan healthcheck dan migrasi otomatis, container yang saling bicara lewat DNS Compose, dan integration test yang berjalan dari dalam stack. Semua kebiasaan baik yang kita tanam di sini — image ramping, konfigurasi tervalidasi, definisi di berkas, pembuktian dengan uji — akan naik kelas mengikuti tuntutan aplikasi dinamis.
Referensi #
- docker container logs — referensi pembacaan log container beserta flag penyaringnya.
- Start containers automatically
— semantik resmi kebijakan restart termasuk
unless-stopped. - Docker Compose CLI
— daftar perintah
docker compose stop,start, dandown.
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>