Resep sudah tertulis dalam tiga berkas; sekarang tangan kita menyentuh mesin. Bagian ini adalah tempat Dockerfile benar-benar dieksekusi: BuildKit membaca resep, menyaring context, menyusun layer, dan menyerahkan image bernama container-web-statis:1.0. Kita menjalankannya dulu secara manual lewat docker run — bukan karena itu cara terbaik, melainkan karena tiap flag terasa kelayakannya saat diketik sendiri — sebelum memindahkan seluruh definisi ke compose.yaml dan menutup dengan satu docker compose up. Di akhir bagian, situs Kopi Nusantara hidup di port 8080 dan siap diuji.

Membangun Image dengan docker build #

Pastikan terminal berada di dalam folder proyek — titik pada akhir perintah berikut menunjuk ke sana sebagai build context:

bash
$ docker build -t container-web-statis:1.0 .

Output yang Dihasilkan:

text
[+] Building 4.0s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 686B                                    0.0s
 => [internal] load .dockerignore                                       0.0s
 => => transferring context: 91B                                        0.0s
 => [internal] load metadata for docker.io/library/nginx:1.29-alpine    1.2s
 => [auth] nginx:1.29-alpine pull token from registry-1.docker.io       0.8s
 => [internal] load build context                                       0.1s
 => => transferring context: 3.982kB                                    0.0s
 => [1/6] FROM docker.io/library/nginx:1.29-alpine                      0.0s
 => => resolve docker.io/library/nginx:1.29-alpine                      1.2s
 => [2/6] LABEL org.opencontainers.image.title="container-web-statis" ...   0.0s
 => [3/6] COPY nginx.conf /etc/nginx/conf.d/default.conf                0.1s
 => [4/6] COPY site/ /usr/share/nginx/html                              0.0s
 => [5/6] EXPOSE 80                                                     0.0s
 => [6/6] RUN nginx -t                                                  0.2s
 => => # nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
 => => # nginx: configuration file /etc/nginx/nginx.conf test is successful
 => exporting to image                                                  0.2s
 => => exporting layers                                                 0.1s
 => => writing image sha256:8c4f2a7d19e3b5c7e2f4a91d06b8c3e5d7f2a4b6c8e1d3f5a7b9c2e4d6f8a1b3 0.1s
 => => naming to docker.io/library/container-web-statis:1.0             0.0s

Baris LABEL dipotong agar muat — lengkapnya empat pasang kunci-nilai OCI yang kita tulis di Bagian 2. Mari kita telaah anatomi build ini:

  1. [+] Building 4.0s (12/12) FINISHED — ringkasan di puncak: dua belas langkah selesai dalam empat detik. Bandingkan dengan build myapp pada Chapter docker build yang makan 23,9 detik — selisihnya bukan mesin yang lebih cepat, melainkan context yang ramping berkat .dockerignore.
  2. load build definition + transferring dockerfile: 686B — BuildKit membaca berkas Dockerfile kita sepanjang 686 byte melalui jalurnya sendiri; inilah jalur terpisah yang membuat Dockerfile tetap terbaca meski namanya tersaring — kejutan yang kita bahas di Bagian 2.
  3. load .dockerignore + transferring context: 91B — penyaring ditemukan dan ikut tercatat sebagai context; bandingkan dengan 2B tanda penyaring kosong pada output-output section Image Build dan Optimisasi.
  4. load build context + transferring context: 3.982kB — seluruh bahan yang lolos penyaring: Dockerfile 686 byte, .dockerignore 91 byte, nginx.conf 869 byte, compose.yaml 182 byte, dan site/index.html 2.154 byte — total 3.982 kilobyte. Angka inilah buah nyata lima baris .dockerignore kita: tanpa penyaring, README.md, materi bagian/, dan .git ikut menaikkan angka ini.
  5. [1/6] FROM dengan durasi 0.0s — basis tidak diunduh ulang: nginx:1.29-alpine sudah tersimpan lokal dari section-section sebelumnya, dan baris resolve 1,2 detik adalah pengesahan bahwa digest lokal masih cocok dengan metadata terbaru — persis perilaku yang kita kenal di docker build.
  6. [6/6] RUN nginx -t dengan dua baris keluaran — jangkar keselamatan Bagian 2 bekerja: syntax is ok dan test is successful tercetak sebagai bagian dari langkah build. Dua baris kecil ini adalah beda antara image yang gagal di meja kita dan container yang crash di depan pengunjung. Setelahnya, exporting to image merakit layer, writing image sha256:8c4f… menetapkan identitas content-addressable, dan naming to menempel tag container-web-statis:1.0.

Image Baru di Penyimpanan Lokal #

Mari kita sapa image hasil kerja kita:

bash
$ docker images

Output yang Dihasilkan:

text
REPOSITORY             TAG           IMAGE ID       CREATED         SIZE
container-web-statis   1.0           8c4f2a7d19e3   2 minutes ago   48.2MB
nginx                  1.29-alpine   5b1e8c3a9f7d   5 days ago      47.9MB

Dua baris ini bercerita banyak. Image kita berukuran 48.2MB — hanya setipis di atas 47.9MB milik nginx:1.29-alpine, karena tambahan kita memang cuma dua layer COPY kecil, label, dan metadata; seluruh badan image adalah basis yang diwarisi layer demi layer. Kolom IMAGE ID 8c4f2a7d19e3 adalah dua belas karakter pertama dari sha256:8c4f2a7d19e3… pada output build — dua wajah untuk objek yang sama, persis relasi ID pendek dan ID penuh yang kita kenal di Melihat Image Lokal. Dan CREATED 5 days ago pada baris nginx mengingatkan bahwa kolom itu milik si penerbit image, bukan milik kita — sementara baris pertama benar-benar berumur dua menit karena kita sendiri yang baru membangunnya.

Menjalankan Container Secara Manual #

Image siap; waktunya menghidupkannya. Kita pakai empat flag sekaligus:

bash
$ docker run -d --name web-statis -p 8080:80 container-web-statis:1.0

Output yang Dihasilkan:

text
4d7b2e9a1f38c52a9071e6b8d4f3a2c5e81b7d9f0a3c6e2b1d4f7a8c5e3b2d6f

Output mode detached selalu satu baris: ID container penuh sepanjang 64 karakter heksadesimal. Mari kita bedah perintahnya:

  1. -d (detached) — container berjalan di latar belakang dan terminal langsung kembali ke kita; nginx memang dirancang hidup lama, menahan terminal hanya akan menyandera kita.
  2. --name web-statis — nama pilihan sendiri menggantikan nama acak dua kata; seluruh perintah berikutnya cukup menyebut web-statis.
  3. -p 8080:80 — menerbitkan port dengan pola host:container: permintaan ke port 8080 di mesin kita diteruskan ke port 80 yang didengarkan nginx di dalam container — port yang sama dengan listen 80 di nginx.conf dan EXPOSE 80 di Dockerfile. Mekanisme lengkapnya adalah wilayah Port Publishing.
  4. container-web-statis:1.0 — image yang dijalankan, ditulis persis tag hasil build; berbeda dari kebiasaan lama mengetik nginx telanjang di Menjalankan Container Pertama, kali ini image-nya adalah milik kita sendiri.

Kondisi container bisa dicek sekejap:

bash
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE                      COMMAND                  CREATED         STATUS         PORTS                                     NAMES
4d7b2e9a1f38   container-web-statis:1.0   "/docker-entrypoint.…"   3 minutes ago   Up 3 minutes   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   web-statis

Kolom COMMAND menampilkan "/docker-entrypoint.…" — skrip entrypoint bawaan image nginx yang kita warisi dengan sengaja tidak menulis CMD di Bagian 2. Kolom PORTS mengonfirmasi pemetaan 8080->80 untuk IPv4 dan IPv6, dan STATUS Up 3 minutes berarti proses nginx hidup sebagai PID 1 di dalamnya. Situs sebenarnya sudah bisa dibuka di browser pada port 8080 — tapi tahan dulu: kita pindahkan definisinya ke Compose dulu, lalu menguji semuanya dengan tertib di Bagian 4.

compose.yaml Satu Service #

Empat flag barusan bekerja, tetapi mereka hidup hanya di riwayat terminal — tidak di repositori. Berkas berikut memindahkan seluruhnya menjadi deklarasi:

yaml
name: container-web-statis

services:
  web:
    build: .
    image: container-web-statis:1.0
    container_name: web-statis
    ports:
      - "8080:80"
    restart: unless-stopped

Sepuluh baris ini menggantikan docker build dan docker run kita. Mari kita telaah ketujuh kuncinya:

  1. name: container-web-statis — nama project Compose; network yang dibuat otomatis nanti bernama container-web-statis_default mengikuti nama ini, dan tanpa container_name, nama itu juga menjadi prefiks nama container.
  2. services: dan web: — satu service bernama web; nama service adalah identitas resminya di seluruh perintah Compose dan kelak menjadi nama DNS-nya di dalam network — bekal yang baru terasa di proyek multi-service.
  3. build: . — service ini dibangun dari Dockerfile pada context folder saat ini; padanan langsung titik pada perintah docker build kita.
  4. image: container-web-statis:1.0 — nama tag untuk hasil build-nya. Kombinasi build dan image inilah yang kita kenal di Image, Build, dan Pull: Compose membangun imagenya sendiri lalu menyimpannya dengan nama yang kita tentukan, bukan nama acak.
  5. container_name: web-statis — nama container yang sama dengan sesi manual kita; menimpa pola bawaan <project>-<service>-<index> yang kita bedah pada Service.
  6. ports: dengan "8080:80" — padanan flag -p, ditulis sebagai daftar. Tanda kutipnya bukan hiasan: YAML generasi lama membaca pola xx:yy tanpa kutip sebagai bilangan berbasis 60 — jebakan sexagesimal yang kita bahas tuntas di Port dan Network.
  7. restart: unless-stopped — kebijakan bangkit-otomatis dari Restart Policy: container dihidupkan ulang bila prosesnya mati sendiri, tapi keputusan manusia untuk menghentikannya dihormati — nuansa yang kita buktikan langsung di Bagian 4.

Perhatikan pula yang tidak tertulis: kunci version:. Ia sudah obsolete — Compose membaca versi spesifikasinya dari dirinya sendiri — dan menuliskannya hanya menambah kebisingan, persis kebiasaan yang kita buang sejak Pengantar Docker Compose.

Dari docker run ke docker compose up #

Dengan compose.yaml tersimpan, satu perintah seharusnya menyalakan semuanya:

bash
$ docker compose up -d --build

Output yang Dihasilkan:

text
Error response from daemon: Conflict. The container name "/web-statis" is already in use by container "4d7b2e9a1f38c52a9071e6b8d4f3a2c5e81b7d9f0a3c6e2b1d4f7a8c5e3b2d6f". You have to remove (or rename) that container to be able to reuse that name.

Bukan bug — ini tabrakan yang sehat. Container web-statis dari sesi manual kita masih hidup dan memegang dua hak milik yang tidak bisa dibagi: nama tersebut dan port 8080. Compose ingin menciptakan container barunya dengan container_name dan ports yang sama, dan daemon menolak. Solusinya satu langkah — pamitkan container manual itu:

bash
$ docker rm -f web-statis

Output yang Dihasilkan:

text
web-statis

Flag -f (force) menghentikan lalu menghapus dalam satu perintah — padanan docker stop diikuti docker rm yang kita kenal di Menghentikan dan Menjalankan Container dan Menghapus Container. Sekarang panggung kosong dan perintah yang sama berjalan mulus:

bash
$ docker compose up -d --build

Output yang Dihasilkan:

text
[+] Building 1.8s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                    0.0s
 => => transferring dockerfile: 686B                                    0.0s
 => [internal] load .dockerignore                                       0.0s
 => => transferring context: 91B                                        0.0s
 => [internal] load metadata for docker.io/library/nginx:1.29-alpine    1.1s
 => [auth] nginx:1.29-alpine pull token from registry-1.docker.io       0.7s
 => [internal] load build context                                       0.1s
 => => transferring context: 3.982kB                                    0.0s
 => CACHED [1/6] FROM docker.io/library/nginx:1.29-alpine               0.0s
 => => resolve docker.io/library/nginx:1.29-alpine                      1.1s
 => CACHED [2/6] LABEL org.opencontainers.image.title="container-web-statis" ...   0.0s
 => CACHED [3/6] COPY nginx.conf /etc/nginx/conf.d/default.conf         0.0s
 => CACHED [4/6] COPY site/ /usr/share/nginx/html                       0.0s
 => CACHED [5/6] EXPOSE 80                                              0.0s
 => CACHED [6/6] RUN nginx -t                                           0.0s
 => exporting to image                                                  0.1s
 => => exporting layers                                                 0.0s
 => => writing image sha256:8c4f2a7d19e3b5c7e2f4a91d06b8c3e5d7f2a4b6c8e1d3f5a7b9c2e4d6f8a1b3 0.0s
 => => naming to docker.io/library/container-web-statis:1.0             0.0s
[+] Running 2/2
 ✔ Network container-web-statis_default  Created                         0.2s
 ✔ Container web-statis                  Started                         0.4s

Build kedua ini berjalan di atas jejak build pertama: seluruh langkah Dockerfile tampil CACHED karena tak satu berkas context pun berubah sejak build pertama — nginx -t sekalipun tidak dieksekusi ulang, hanya diingat. Kemudian bagian [+] Running 2/2 menghidupkan stack: network container-web-statis_default diciptakan otomatis, dan container web-statis — kini di bawah pengawasan Compose — menyala. Perintah yang sama akan kita pakai selamanya ke depan: up -d --build membangun bila perlu, memakai cache bila bisa, dan menyala dalam dua detik.

Dua gaya menjalankan situs yang sama kini pernah kita rasa. Tabel ini merangkum padanannya:

Aspekdocker run manualCompose
Nama container--name web-statiscontainer_name: web-statis
Penerbitan port-p 8080:80ports: ["8080:80"]
Kebijakan restart--restart unless-stoppedrestart: unless-stopped
Pembangunan imageperintah docker build terpisahbuild: . menyatu dengan definisi
Eksekusi ulangmengingat seluruh flag dari riwayat terminaldocker compose up -d --build

Container hidup di bawah Compose, port 8080 menunggu diketuk, dan seluruh konfigurasi Bagian 2 menahan napas untuk pembuktian. Bagian berikutnya adalah sidangnya: kita membaca sepuluh baris respons curl -I satu per satu, memburu 404 untuk membuktikan try_files, menimbang gzip byte-per-byte, membaca jejak lima permintaan di docker logs, lalu mematikan dan menghidupkan kembali semuanya untuk melihat siapa yang bangkit sendiri. Saatnya membuktikan bahwa semua janji konfigurasi benar-benar ditepati.

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>