Kerangka network dari Bagian 1 menahan satu janji yang belum ditepati: berkas ./nginx/nginx.conf yang di-bind-mount ke /etc/nginx/nginx.conf masih belum ada isinya, dan tanpa isinya container proxy bahkan tidak akan menyala. Bagian ini menulis otak pintu depan itu dari baris pertama sampai terakhir — bukan potongan location lepas seperti latihan pintu di chapter Reverse Proxy, melainkan berkas konfigurasi utama yang lengkap dengan events dan http. Setiap bloknya kita bedah, satu perilaku paling menentukan — trailing slash pada proxy_pass — kita buktikan lewat tabel path, dan keputusan memasang berkas utama alih-alih gaya conf.d kita kontraskan dengan proyek pertama kita.

nginx.conf Utuh #

Buat folder nginx/ di akar proyek lalu ketik berkas nginx/nginx.conf dengan isi persis berikut:

conf
# Reverse proxy untuk routing ke dua service backend.
# Container proxy hanya membaca konfigurasi ini; seluruh resolusi
# nama memakai DNS internal jaringan Compose.

events {
    worker_connections 1024;
}

http {
    # Nama service Compose dipakai langsung sebagai alamat upstream.
    upstream svc_a {
        server service-a:8080;
    }

    upstream svc_b {
        server service-b:8080;
    }

    server {
        listen 80;

        # Indeks routing: satu pintu di port 80, dua jalur prefix.
        location = / {
            default_type application/json;
            return 200 '{"proxy":"nginx","routes":["/a/*","/b/*"]}\n';
        }

        # Prefix /a/ dibuang sebelum diteruskan ke service-a
        # (perhatikan garis miring di akhir proxy_pass).
        location /a/ {
            proxy_pass http://svc_a/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location /b/ {
            proxy_pass http://svc_b/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Empat puluh empat baris ini adalah seluruh kecerdasan stack kita. Mari kita telaah ketujuh bloknya:

  1. events { … } — blok wajib berkas utama; tanpa satu baris ini pun nginx menolak menyala. worker_connections 1024 membatasi jumlah koneksi yang bisa ditangani tiap worker — angka konservatif yang berlaku untuk seluruh latihan kita.
  2. http { … } — rumah seluruh directive berdomain HTTP: upstream, server, logging, dan proxying hidup di dalamnya. Berkas utama nginx selalu berpola events lalu http — dua blok paling luar dalam berkas ini.
  3. upstream svc_a { server service-a:8080; } — mendeklarasikan kelompok server bernama svc_a berisi satu anggota. Perhatikan alamatnya: service-a adalah nama service Compose, bukan alamat IP — nama itu diterjemahkan embedded DNS jaringan proxy-latihan_default, persis mekanisme yang kita buktikan di chapter Container DNS.
  4. upstream svc_b — kembaran untuk service-b. Dua upstream terpisah membuat routing tetap terbaca manusia; kelak saat butuh load balancing, cukup tambahkan baris server kedua di dalam satu upstream.
  5. server { listen 80; } — proxy mendengarkan port 80 di dalam container; port inilah yang diterbitkan ke host sebagai 8080 lewat pemetaan "8080:80" di Bagian 1. Kita tidak menulis server_name — satu server block menerima semua permintaan yang tiba.
  6. location /a/ dan location /b/ — dua jalur prefix. nginx mencocokkan awalan jalur terpanjang: permintaan /a/halo jatuh ke blok pertama, /b/halo ke blok kedua, dan keduanya meneruskan lewat proxy_pass dengan tiga header identitas klien.
  7. location = / — pencocokan eksak: tanda = berarti blok ini hanya menangani permintaan ke / persis, bukan prefiks. default_type application/json mengatur header Content-Type, dan return 200 '…' menjawab sendiri tanpa menyentuh backend — indeks routing yang kita lihat di halaman overview, string literal sepanjang 42 karakter plus \n.

Trailing Slash pada proxy_pass #

Satu karakter paling menentukan di seluruh berkas ini adalah garis miring di ujung alamat tujuan proxy_pass menuju svc_a. Aturannya kita kenal dari latihan pintu di chapter Reverse Proxy: saat proxy_pass memuat URI — sekecil apa pun, termasuk sekadar / — nginx mengganti bagian jalur yang cocok dengan location memakai URI tersebut. Tabel berikut memetakan apa yang diterima backend untuk setiap permintaan:

Permintaan ke proxylocation yang cocokDiterima backend — dengan trailing slashDiterima backend — tanpa trailing slash
/a/halo/a//halo/a/halo
/a//a///a/
/a/sub/laporan/a//sub/laporan/a/sub/laporan

Kolom terakhir adalah nasib konfigurasi ketika alamat proxy_pass ditulis tanpa garis miring di ujungnya: tanpa URI di ujung, jalur diteruskan apa adanya, dan backend menerima /a/halo lengkap dengan awalannya. Untuk service kita itu bukan bencana — server.py menjawab semua path — tetapi untuk backend sungguhan yang punya rute /halo dan tidak punya /a/halo, satu karakter ini adalah beda antara 200 dan 404. Aturan praktisnya sama seperti di chapter Reverse Proxy: awalan path di location hampir selalu berarti awalan yang harus dibuang, dan trailing slash di proxy_pass adalah alat pembuangnya. Bukti kolom ketiga kita saksikan langsung lewat field "path" pada respons JSON di Bagian 4.

Tiga Header Identitas Klien #

Ketiga baris proxy_set_header di setiap location bukan pajangan — tanpa mereka, backend melihat dunia yang salah:

  1. proxy_set_header Host $host — tanpa baris ini, nginx mengirim header Host dengan nilai $proxy_host, yaitu nama upstream seperti tertulis di proxy_pass (svc_a) — backend akan mengira ia dipanggil dengan nama itu. $host mengirim ulang nama host yang diminta klien asli (localhost:8080), sehingga aplikasi yang memvirtual-host berdasarkan nama menjawab dengan konfigurasi yang benar.
  2. proxy_set_header X-Real-IP $remote_addr — membawa satu alamat IP klien asli persis seperti dilihat proxy. Dari sisi backend, setiap permintaan tampak datang dari alamat container proxy; header ini mengembalikan wajah klien yang sesungguhnya.
  3. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for — membawa rantai alamat: nilai X-Forwarded-For yang sudah ada bila permintaan sebelumnya melewati proxy lain, ditambah $remote_addr — standar de facto yang dipatuhi rantai proxy mana pun.

server.py kita belum membaca satu pun header itu — dan memang tidak perlu untuk latihan routing. Tetapi menuliskannya sejak dini adalah kebiasaan yang dibayar mahal bila backend kelak berganti aplikasi sungguhan yang menghitung pengunjung, membatasi IP, atau menyusun URL absolut dari header Host.

Berkas Utama, Bukan conf.d #

Keputusan memasang konfigurasi ke /etc/nginx/nginx.conf — dan bukan ke /etc/nginx/conf.d/ — adalah keputusan arsitektur, bukan selera. Berkas utama image nginx punya struktur bertingkat: ia memuat blok events dan http, dan di dalam blok http itulah tercantum include /etc/nginx/conf.d/*.conf; — semua berkas conf.d dimuat sebagai isi blok http. Konsekuensinya tegas: berkas gaya conf.d hanya boleh berisi server, upstream, dan directive http lainnya — blok events dan http tidak boleh muncul di dalamnya, karena akan berarti blok http bersarang di dalam http, dan nginx menolak menyala.

Kontrasnya persis dengan proyek pertama kita. Di Proyek: Container Web Statis, berkas nginx.conf berisi satu blok server polos — gaya conf.d — dan di-COPY ke /etc/nginx/conf.d/default.conf lewat Dockerfile; blok events dan http tidak pernah kita tulis karena berkas utama bawaan image sudah menyediakannya. Di proyek ini urutannya dibalik: kita menimpa berkas utama itu sendiri lewat bind mount, sehingga events dan http menjadi tanggung jawab kita — kebebasan penuh atas worker_connections dan kelak atas directive http level atas seperti log_format.

Dua cara memasang berkas itu juga membawa trade-off yang layak dilihat:

AspekBind mount ke berkas utamaCOPY di Dockerfile
Mengubah konfigurasiedit berkas di host, reload — tanpa rebuildwajib rebuild image
Image yang mandiritidak — image proxy butuh berkas host saat runtimeya — image membawa konfigurasinya sendiri
Cocok untukdevelopment dan latihanproduction dan distribusi

Untuk latihan di mesin sendiri, bind mount menang jelas: kita akan mengubah-halus nginx.conf beberapa kali tanpa sekali pun menunggu build. Akhiran :ro menegaskan arah kepercayaan — container hanya boleh membaca berkas itu, pola Mount Read-Only dari section Penyimpanan Data.

Konfigurasi pintu depan kini lengkap dan menunggu penghuni belakang: dua nama di upstream — service-a dan service-b — belum punya container yang menjawabnya, karena Dockerfile dan server.py belum kita tulis. Bagian 3: Menghubungkan Service melahirkan kedua backend itu, menyuntik identitas biru dan hijau lewat environment, lalu menyalakan seluruh stack dengan satu docker compose up -d --build.

Referensi #

  • nginx Documentation — directive events, http, upstream, location, return, dan perilaku trailing slash pada proxy_pass.
  • Compose file reference — kunci volumes untuk bind mount berkas konfigurasi ke dalam service.

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