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-alpinesebagai 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, beridentitasservice-awarnabiru, menghidupi seluruh jalur/a/*, - service-b — kembaran service-a dengan identitas berbeda (
service-b, warnahijau) lewat imageproxy-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:
$ docker compose up -d --buildOutput yang Dihasilkan (dipotong):
[+] 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.7sBagian 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:
$ curl http://localhost:8080/Output yang Dihasilkan:
{"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:
$ curl http://localhost:8080/a/haloOutput yang Dihasilkan:
{"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.yamltiga service dengan kombinasibuild+imageuntuk backend buatan sendiri danexposesebagai dokumentasi port, - menulis
nginx.confutuh 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, dandocker 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
curltersedia 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 downdari 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:
reverse-proxy-multi-service/
├── compose.yaml
├── nginx/
│ └── nginx.conf
├── services/
│ ├── service-a/
│ │ ├── Dockerfile
│ │ └── server.py
│ └── service-b/
│ ├── Dockerfile
│ └── server.py
└── README.mdPeran setiap berkas dan bagian tempat ia lahir:
| Berkas | Peran | Bagian |
|---|---|---|
compose.yaml | Deklarasi tiga service: proxy, service-a, service-b | Bagian 1 |
nginx/nginx.conf | Konfigurasi utama nginx: upstream, routing path, indeks JSON | Bagian 2 |
services/service-a/Dockerfile | Resep image proxy-service-a:1.0 | Bagian 3 |
services/service-a/server.py | HTTP server backend Python tanpa dependensi | Bagian 3 |
services/service-b/Dockerfile | Resep image proxy-service-b:1.0 — kembaran dengan identitas berbeda | Bagian 3 |
services/service-b/server.py | Salinan server backend untuk service-b | Bagian 3 |
README.md | Ringkasan cara menjalankan dan menguji proyek | — |
Tahapan #
Proyek dikerjakan dalam empat bagian berurutan. Setiap bagian berdiri di atas berkas bagian sebelumnya:
- Bagian 1: Merancang Network — topologi satu pintu, diagram jaringan, dan
compose.yamlutuh tiga service beserta bedahportsversusexpose. - Bagian 2: Mengonfigurasi Reverse Proxy —
nginx.confdari nol: upstream, routing path, trailing slash, tiga header, dan perbedaan berkas utama versus gayaconf.d. - Bagian 3: Menghubungkan Service — Dockerfile dan
server.pyuntuk dua backend, pola satu kode dua identitas, laludocker compose up -d --buildpertama. - 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.
Rancang topologi jaringan dan susun compose.yaml tiga service.
Buka → # Bagian 2: Mengonfigurasi Reverse ProxyTulis nginx.conf dengan upstream dan routing path.
Buka → # Bagian 3: Menghubungkan ServiceBangun image service dan sambungkan lewat DNS Compose.
Buka → # Bagian 4: Menguji RoutingUji routing, partial failure, dan pemulihan service.
Buka → # Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
compose.yaml
Unduh berkasname: 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#!/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#!/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()