Tiket terakhir yang dijanjikan penutup proyek WordPress dan MySQL kini tiba di tangan kita, dan sekaligus menutup seluruh Course Docker. Tidak ada materi baru yang diperkenalkan di sini — proyek ini adalah panggung tempat hampir semua keahlian kita dipakai bersamaan dalam satu tarikan napas: jaringan container dengan DNS internal, compose.yaml tiga service, build image dengan BuildKit, routing nginx berbasis path, restart policy, sampai membaca log saat salah satu service jatuh. Seperti empat proyek sebelumnya, hasil akhirnya harus bisa dibuktikan lewat perintah sederhana dengan output yang bisa kita jelaskan asal-usulnya byte demi byte. Dan karena inilah proyek pamungkas, bagian terakhirnya juga merangkum seluruh perjalanan kita sejak docker run hello-world yang pertama.

Apa yang Kita Bangun #

Kita membangun stack tiga service dengan pembagian peran yang tegas:

  • proxy — container nginx:1.29-alpine sebagai satu-satunya pintu publik; ia mendengarkan port 80 di dalam container yang diterbitkan ke port host 8080, dan memutuskan permintaan mana yang diteruskan ke service mana berdasarkan awalan path,
  • service-a — HTTP server Python murni yang dibangun sendiri menjadi image proxy-service-a:1.0, beridentitas service-a warna biru, menghidupi seluruh jalur /a/*,
  • service-b — kembaran service-a dengan identitas berbeda (service-b, warna hijau) lewat image proxy-service-b:1.0, menghidupi seluruh jalur /b/*.

Dua service backend itu tidak menerbitkan port apa pun ke host. Mereka hanya hidup di dalam network Compose dan dihubungi proxy lewat nama service — pola yang di section Aplikasi Multi-Container kita kenal sebagai dasar arsitektur microservice. Klien dari luar hanya mengenal satu alamat: localhost:8080. Jalur /a/ dan /b/-lah yang memutuskan jawaban datang dari service mana, dan proxy membuang awalan itu sebelum meneruskan permintaan — detail kecil yang kita buktikan lewat field path pada respons JSON backend.

Hasil Akhir dalam Dua Perintah #

Seluruh proyek bermuara pada dua perintah berikut, dijalankan dari folder proyek setelah keempat bagian selesai:

bash
$ docker compose up -d --build

Output yang Dihasilkan (dipotong):

text
[+] Building 8.7s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                    0.0s
 => => transferring dockerfile: 401B                                    0.0s
...
 => => naming to docker.io/library/proxy-service-b:1.0                  0.0s
[+] Running 4/4
 ✔ Network proxy-latihan_default        Created                         0.2s
 ✔ Container proxy-latihan-service-a-1  Started                         0.5s
 ✔ Container proxy-latihan-service-b-1  Started                         0.4s
 ✔ Container proxy-latihan-proxy-1      Started                         0.7s

Bagian tengah build dipotong agar ringkas — Bagian 3 membacanya baris demi baris, termasuk mengapa dua image dibangun dan mengapa urutan container mengikuti depends_on. Perintah kedua mengetuk pintunya:

bash
$ curl http://localhost:8080/

Output yang Dihasilkan:

json
{"proxy":"nginx","routes":["/a/*","/b/*"]}

Indeks routing itu digenerasi nginx sendiri — string literal pada location = / di nginx.conf, sepanjang empat puluh dua karakter plus satu baris baru. Satu permintaan lagi ke jalur /a/ membuktikan routing sungguhan:

bash
$ curl http://localhost:8080/a/halo

Output yang Dihasilkan:

json
{"service": "service-a", "color": "biru", "path": "/halo"}

Tiga field itu adalah kartu nama proyek kita: "service": "service-a" adalah bukti ENV SERVICE_NAME di Dockerfile sampai ke payload, "path": "/halo" adalah bukti proxy membuang awalan /a/ sebelum meneruskan permintaan, dan "color" menegaskan kedua kembaran mudah dibedakan. Bagian 4 menguji semua jalur ini — termasuk jalur yang tidak ada dan saat salah satu backend dimatikan sementara.

Tujuan Pembelajaran #

Setelah menyelesaikan proyek ini, kita mampu:

  • merancang topologi satu pintu: satu port host yang diterbitkan untuk seluruh stack, service backend murni internal,
  • menyusun compose.yaml tiga service dengan kombinasi build + image untuk backend buatan sendiri dan expose sebagai dokumentasi port,
  • menulis nginx.conf utuh dari nol — events, http, upstream, location, sampai indeks JSON — dan memasangnya sebagai berkas utama lewat bind mount read-only,
  • memanfaatkan DNS Compose: nama service sebagai alamat upstream, dengan segala konsekuensi statisnya saat service berhenti dan bangkit kembali,
  • menguji routing dan kegagalan parsial dengan curl, docker compose ps, dan docker compose logs, lalu memulihkan service tanpa menyentuh sisanya.

Prasyarat #

Proyek ini tidak memperkenalkan konsep baru; ia merangkai yang lama. Pastikan mesin latihan memenuhi hal berikut:

  • Docker Engine 29.9.0 dengan plugin Compose v5.6.0 terpasang dan lulus ritual Verifikasi Instalasi,
  • klien curl tersedia di host — Bagian 4 memakainya sebagai alat uji utama,
  • tidak ada stack lain yang menempati port 8080; bila sisa latihan masih menyala, bongkar dulu dengan docker compose down dari foldernya.

Chapter yang paling sering dirujuk datang dari tiga section. Dari section Jaringan Container: Konsep Networking Docker, Membuat Custom Network, dan Container DNS — dasar mengapa nama service bisa menjadi alamat. Dari section Docker Compose: Struktur compose.yaml, Image, Build, dan Pull, depends_on dan Startup Order, serta Menjalankan dan Mengelola Stack — kerangka deklaratif seluruh proyek. Dari section Aplikasi Multi-Container: Arsitektur Multi-Container, Reverse Proxy, dan Graceful Shutdown — pola satu pintu, perilaku proxy_pass, dan makna Exited (143) yang akan kita jumpai di Bagian 4.

Peta File Proyek #

Struktur lengkap proyek yang kita bangun bertahap:

text
reverse-proxy-multi-service/
├── compose.yaml
├── nginx/
│   └── nginx.conf
├── services/
│   ├── service-a/
│   │   ├── Dockerfile
│   │   └── server.py
│   └── service-b/
│       ├── Dockerfile
│       └── server.py
└── README.md

Peran setiap berkas dan bagian tempat ia lahir:

BerkasPeranBagian
compose.yamlDeklarasi tiga service: proxy, service-a, service-bBagian 1
nginx/nginx.confKonfigurasi utama nginx: upstream, routing path, indeks JSONBagian 2
services/service-a/DockerfileResep image proxy-service-a:1.0Bagian 3
services/service-a/server.pyHTTP server backend Python tanpa dependensiBagian 3
services/service-b/DockerfileResep image proxy-service-b:1.0 — kembaran dengan identitas berbedaBagian 3
services/service-b/server.pySalinan server backend untuk service-bBagian 3
README.mdRingkasan cara menjalankan dan menguji proyek—

Tahapan #

Proyek dikerjakan dalam empat bagian berurutan. Setiap bagian berdiri di atas berkas bagian sebelumnya:

  1. Bagian 1: Merancang Network — topologi satu pintu, diagram jaringan, dan compose.yaml utuh tiga service beserta bedah ports versus expose.
  2. Bagian 2: Mengonfigurasi Reverse Proxy — nginx.conf dari nol: upstream, routing path, trailing slash, tiga header, dan perbedaan berkas utama versus gaya conf.d.
  3. Bagian 3: Menghubungkan Service — Dockerfile dan server.py untuk dua backend, pola satu kode dua identitas, lalu docker compose up -d --build pertama.
  4. Bagian 4: Menguji Routing — indeks JSON, dua jalur, jalur tak dikenal, kegagalan parsial dan pemulihannya, kronologi log proxy, pembongkaran, serta rangkuman seluruh course.

Mulailah dari Bagian 1 dan pertahankan urutannya: konfigurasi nginx di Bagian 2 hanya masuk akal di atas deklarasi network Bagian 1, dan angka-angka di Bagian 4 hanya bermakna bila output build di Bagian 3 sudah kita pahami. Ini proyek penutup — kerjakan pelan-pelan dan nikmati bagian terakhirnya.

Referensi #

  • Docker Compose — kerangka deklaratif tempat seluruh topologi proyek ini hidup.
  • nginx — Docker Hub — halaman image resmi yang menjadi pintu depan stack, beserta perilaku berkas konfigurasi utamanya.

Berkas Sumber

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

compose.yaml

Unduh berkas
yaml
name: proxy-latihan

services:
  proxy:
    image: nginx:1.29-alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "8080:80"
    depends_on:
      - service-a
      - service-b
    restart: unless-stopped

  service-a:
    build: ./services/service-a
    image: proxy-service-a:1.0
    expose:
      - "8080"
    restart: unless-stopped

  service-b:
    build: ./services/service-b
    image: proxy-service-b:1.0
    expose:
      - "8080"
    restart: unless-stopped

services/service-a/server.py

Unduh berkas
python
#!/usr/bin/env python3
"""Service backend minimal untuk latihan reverse proxy.

Menyajikan satu endpoint GET yang melaporkan nama service, warna,
dan path yang diminta — cukup untuk membuktikan routing nginx
dan pembuangan prefix oleh proxy_pass.
"""

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os

SERVICE_NAME = os.environ.get("SERVICE_NAME", "service-a")
SERVICE_COLOR = os.environ.get("SERVICE_COLOR", "biru")
PORT = int(os.environ.get("PORT", "8080"))


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        payload = json.dumps(
            {"service": SERVICE_NAME, "color": SERVICE_COLOR, "path": self.path}
        ) + "\n"
        body = payload.encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        print(
            "%s - - [%s] %s"
            % (self.address_string(), self.log_date_time_string(), fmt % args),
            flush=True,
        )


if __name__ == "__main__":
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()

services/service-b/server.py

Unduh berkas
python
#!/usr/bin/env python3
"""Service backend minimal untuk latihan reverse proxy.

Menyajikan satu endpoint GET yang melaporkan nama service, warna,
dan path yang diminta — cukup untuk membuktikan routing nginx
dan pembuangan prefix oleh proxy_pass.
"""

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os

SERVICE_NAME = os.environ.get("SERVICE_NAME", "service-a")
SERVICE_COLOR = os.environ.get("SERVICE_COLOR", "biru")


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        payload = json.dumps(
            {"service": SERVICE_NAME, "color": SERVICE_COLOR, "path": self.path}
        ) + "\n"
        body = payload.encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        print(
            "%s - - [%s] %s"
            % (self.address_string(), self.log_date_time_string(), fmt % args),
            flush=True,
        )


if __name__ == "__main__":
    ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()