Setiap proyek sebelumnya dimulai dari berkas aplikasi; kali ini kita dimulai dari peta. Disaster drill WordPress di proyek sebelumnya membuktikan bahwa data bisa dibangkitkan dari backup, dan penutupnya menyerahkan satu tiket: menjadikan penerbitan port sebagai urusan satu pihak. Bagian ini menggambar topologi itu menjadi kenyataan — satu proxy di muka, dua service di belakang, semuanya berdiri di atas satu network Compose — lalu menuangkannya ke compose.yaml utuh yang akan kita bawa sampai akhir proyek. Keputusan terbesar proyek ini justru diambil di sini, sebelum satu baris kode Python ditulis: hanya ada satu pintu.

Topologi Satu Pintu #

Gambaran lengkap stack kita dapat digambar dalam satu diagram:

Mari kita baca diagram itu dari kiri ke kanan. Klien — curl di host, atau browser apa pun — hanya mengenal satu alamat: localhost:8080. Panah pertama melewati pemetaan port yang satu-satunya ada di seluruh stack; setelah panah itu, klien tidak pernah tahu ada berapa service di dalam. Tiga container berdiri di dalam satu network proxy-latihan_default: proxy di depan, service-a dan service-b di belakangnya, dan dua panah terakhir hanya bisa dilalui dari dalam network — tidak ada jalur dari luar yang sampai ke kedua backend. Inilah bedanya dengan draf dua pintu yang kita bedah di chapter Reverse Proxy: satu pintu menghapus masalah CORS ganda, firewall banyak port, dan sertifikat TLS yang tercecer.

Cara paling sederhana memahami desain ini adalah analogi resepsionis. Gedung kantor kita punya banyak ruangan — dua di antaranya dihuni service — tetapi semua tamu harus melapor ke satu meja depan. Resepsionis-lah yang memeriksa keperluan tamu (path permintaan) dan mengantarnya ke ruangan yang tepat; tamu tidak pernah boleh berjalan sendiri ke lorong belakang, dan tidak ada pintu samping yang bisa dibongkar. Ketika gedung menambah ruangan baru, tamu tidak perlu tahu — mereka tetap melapor ke meja yang sama. Satu pintu berarti satu titik yang perlu dijaga, diamati, dan kelak diberi sertifikat TLS.

Satu Network, Tiga Alamat #

Tiga service kita tidak mendeklarasikan network apa pun di compose.yaml — dan itu bukan kelalaian. Compose otomatis menciptakan satu network per project, dinamai <nama-project>_default; karena berkas kita menetapkan name: proxy-latihan, network yang tercipta bernama proxy-latihan_default, terlihat baris per baris pada output docker compose up di Bagian 3. Network otomatis ini adalah user-defined bridge — jenis jaringan yang sejak chapter Membuat Custom Network kita kenal punya satu fitur pemenarik: embedded DNS di alamat 127.0.0.11, yang sejak chapter Container DNS kita buktikan menerjemahkan nama container menjadi alamat IP.

Perangkat itu yang membuat seluruh proyek ini bisa sesederhana kelihatannya. Di dalam proxy-latihan_default, nama service-a dan service-b adalah alamat yang sah — proxy tinggal menulisnya di konfigurasi nginx tanpa pernah tahu alamat IP di baliknya. Begitu pula alasannya mengapa isolasi ini bukan sekadar estetika: backend yang tidak menerbitkan port tidak punya permukaan serangan dari luar host, dan satu-satunya pihak yang boleh berbicara dengan mereka adalah sesama penghuni network. Pelajaran dari chapter Prinsip Keamanan Container berlaku paling murah ketika diterapkan sejak gambar topologi.

compose.yaml Tiga Service #

Berkas pertama dan satu-satunya pada tahap ini adalah compose.yaml di akar folder proyek. Ketik utuh — kita akan membedahnya sepotong-sepotong:

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

Bentuk besarnya akrab dari Struktur compose.yaml: satu blok services berisi tiga service, tanpa blok networks dan tanpa kunci version: yang obsolete. Mari kita telaah tujuh keputusan pentingnya:

  1. name: proxy-latihan — nama project Compose. Dari nama inilah network proxy-latihan_default tercipta, dan nama container mengikuti pola <project>-<service>-<index>: proxy-latihan-proxy-1, proxy-latihan-service-a-1, proxy-latihan-service-b-1 — tiga nama yang akan kita sapa terus di Bagian 4.
  2. image: nginx:1.29-alpine pada proxy — image resmi dipin pada tag eksplisit, kebiasaan yang kita tanam sejak Memahami Image Tag. Proxy tidak kita build sendiri karena tugasnya murni konfigurasi, bukan kode; berkas konfigurasinya justru disuntikkan lewat baris berikutnya.
  3. volumes bind mount ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro — berkas konfigurasi di host menimpa berkas utama nginx di dalam container, dengan :ro agar container tidak bisa mengubahnya — pola Mount Read-Only yang kita bedah tuntas di section Penyimpanan Data. Mengapa targetnya berkas utama dan bukan conf.d adalah cerita panjang di Bagian 2.
  4. ports: "8080:80" — hanya pada proxy — satu-satunya pemetaan host:container di seluruh stack. Permintaan ke port 8080 host diteruskan ke port 80 di dalam container proxy; dua backend tidak punya kunci ports sama sekali. Mekanisme penerbitan port ini adalah wilayah Port Publishing.
  5. depends_on bentuk daftar — proxy dinyalakan setelah service-a dan service-b berjalan. Bentuk daftar hanya mengatur urutan start, bukan kesiapan: ia tidak menunggu backend lulus healthcheck seperti condition: service_healthy pada depends_on dan Startup Order — pembedaan yang kita buktikan konsekuensinya di Bagian 4. Untuk nginx, urutan ini punya satu tugas penting: upstream harus sudah bisa di-resolve DNS ketika nginx memuat konfigurasinya — benang merah yang kita tarik penuh di Bagian 3.
  6. build + image berdampingan pada kedua backend — build: ./services/service-a memberi tahu Compose membangun imagenya dari Dockerfile di folder itu, dan image: proxy-service-a:1.0 memberi nama tag hasil buildnya. Kombinasi kunci ini kita kenal dari Image, Build, dan Pull: bukan nama acak, melainkan identitas yang bisa kita rujuk ulang di docker images.
  7. expose: "8080" pada kedua backend — mendeklarasikan port yang didengarkan service di dalam network, tanpa menerbitkannya ke host. Apa sebenarnya fungsi kunci ini — dan mengapa bilangnya dokumentasi murni — kita bedah tuntas di bawah.

ports versus expose #

Dua kunci yang paling mudah tertukar di seluruh compose.yaml ini layak mendapat meja sendiri. Empat aspek berikut memisahkan mereka:

Aspekportsexpose
Menerbitkan port ke hostya — pemetaan host:container lewat NATtidak — tidak ada pemetaan apa pun
Peran sesungguhnyajalur masuk publikdokumentasi metadata port
Efek pada konektivitas antar-containertidak dibutuhkan — trafik internal sudah jalantidak memberi efek apa pun
Kebutuhan konfigurasipenerbitan firewall dan konflik port harus dipikirkancukup dicatat, tanpa konsekuensi

Baris ketiga tabel adalah kuncinya: komunikasi proxy menuju service-a:8080 tidak membutuhkan kedua kunci ini sama sekali — begitu dua container berada di satu network, semua port yang didengarkan proses di dalamnya bisa diakses sesama penghuni. expose dengan demikian murni dokumentasi: ia memberi tahu pembaca compose.yaml (dan beberapa tool yang membacanya) port mana yang dimaksudkan dipakai, tetapi daemon tidak membuka atau menutup apa pun karenanya. Kedua baris expose bisa dihapus dan routing tetap bekerja — keduanya tetap dipertahankan karena berkas konfigurasi yang mendokumentasikan dirinya sendiri lebih mudah dibaca manusia.

Sebaliknya ports punya efek nyata yang bisa disaksikan: satu baris "8080:80" membuat 0.0.0.0:8080->80/tcp muncul di kolom PORTS pada docker compose ps — satu-satunya baris PORTS terisi di seluruh stack, bukti visual satu pintu yang kita periksa di Bagian 4.

Kerangka network kini berdiri: satu project bernama proxy-latihan, satu network default, satu pintu 8080, dua backend tanpa alamat publik. Tetapi proxy masih membawa konfigurasi bawaan image nginx yang belum tahu apa-apa tentang service kita — berkas nginx/nginx.conf yang di-mount ke /etc/nginx/nginx.conf masih kosong di folder proyek. Bagian 2: Mengonfigurasi Reverse Proxy mengisi otak pintu depan itu: upstream, dua jalur prefix, dan indeks JSON yang menyambut setiap tamu pertama.

Referensi #

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()