Peta arsitektur pada chapter sebelumnya menempatkan proxy sebagai satu-satunya service yang berhak menerbitkan port ke host — tetapi keputusan itu masih berupa klaim. Budi belum merasakan bedanya: draf pertama toko yang ia rakit malah membuka dua pintu publik sekaligus, frontend di port host 8080 dan backend di 8081, dan dalam dua hari tiga masalah muncul satu per satu — permintaan lintas asal yang ditolak browser, firewall yang harus membuka port ganda, dan bayangan sertifikat TLS yang kelak harus dipasang dua kali. Chapter ini melunasi klaim proxy dengan nginx: kita bedah dulu masalah dua pintu itu dan posisi reverse proxy menghadap forward proxy, lalu berlatih cepat dengan project mini bernama pintu, sebelum memasang proxy sungguhan di depan toko — lengkap dengan routing jalur / dan /api/, pembersihan awalan path, serta tiga header yang menjaga identitas klien asli.

Dua Pintu Publik dan Masalahnya #

Draf awal Budi memang sederhana dan tampaknya tidak berbahaya:

yaml
  frontend:
    image: nginx:1.29-alpine
    ports:
      - "8080:80"

  backend:
    image: nginx:1.29-alpine
    ports:
      - "8081:80"

Halaman toko dibuka di localhost:8080, lalu JavaScript pada halaman itu mengambil data produk dari localhost:8081. Masalah pertama datang dari browser: halaman dan data kini berasal dari asal yang berbeda (port berbeda dianggap asal berbeda), sehingga setiap permintaan data adalah permintaan lintas asal — dan tanpa header CORS yang dikirim backend, browser menolak menjawabnya. Aplikasi dipaksa mengurus CORS padahal masalah sebenarnya bukan kebijakan keamanan, melainkan arsitektur yang membocorkan dua alamat publik. Masalah kedua muncul begitu stack mau pindah ke server: firewall harus membuka dua port, dan setiap service tambahan menuntut satu pintu baru. Masalah ketiga menunggu di masa depan — begitu HTTPS dipasang, sertifikat dan konfigurasi TLS harus hidup di dua tempat yang harus disinkronkan manual.

Ketiganya bermuara pada satu keputusan: publik cukup mengenal satu alamat, dan pemecahan alamat ke service yang benar menjadi urusan pihak yang berdiri di depan — reverse proxy.

Reverse Proxy versus Forward Proxy #

Kata “proxy” sering dipakai bergantian untuk dua benda yang berdiri di arah berlawanan. Forward proxy adalah wakil si klien: Siti di kantornya mengenal satu server proxy yang menyaring seluruh trafik keluar karyawan, dan server tujuan hanya melihat si proxy, bukan komputer Siti. Reverse proxy adalah wakil si server: klien memanggil satu alamat publik, dan proxy di baliknya yang memutuskan permintaan itu diteruskan ke service mana — klien tidak pernah tahu ada berapa aplikasi di belakang pintu itu.

AspekForward proxyReverse proxy
Berdiri di sisiklienserver
Yang mengonfigurasinyaklien — browser atau sistem kantoroperator aplikasi
Yang tidak sadar keberadaannyaserver tujuanklien
Menyembunyikanidentitas klien dari servertopologi server dari klien
Contohproxy penyaring trafik keluar kantornginx di depan aplikasi

Lima Manfaat Reverse Proxy #

ManfaatPenjelasan singkat
Satu pintu publikhanya satu port host yang diterbitkan; service lain murni internal
TLS di satu tempatsertifikat dipasang dan diperbarui di proxy saja, bukan di tiap service
Routing berbasis jalur/ dan /api/ bisa diarahkan ke service berbeda lewat konfigurasi
Topologi tersembunyijumlah, nama, dan alamat service internal tidak terpapar ke luar
Log dan header terpusatseluruh akses tercatat di satu tempat dengan identitas klien asli yang diteruskan

Lima baris itu akan kita buktikan satu per satu pada toko — tetapi sebelum mengutak-atik stack besar, ada jalan pintas untuk merasakan mekanismenya dalam lima menit.

Latihan Cepat: Project pintu #

Project mini bernama pintu berisi tiga service nginx: satu proxy dan dua backend sederhana. Foldernya Budi susun di samping folder toko:

text
pintu/
├── compose.yaml
├── pintu.conf
├── web-a/
│   └── index.html
└── web-b/
    └── index.html

Dua halaman mini menjadi isi backend:

html
<h1>Halo dari web-a</h1>
html
<h1>Halo dari web-b</h1>

Konfigurasi proxy-nya — berkas pintu.conf — adalah reverse proxy terkecil yang masih berguna:

conf
server {
    listen 80;

    location /web-a/ {
        proxy_pass http://web-a:80/;
    }

    location /web-b/ {
        proxy_pass http://web-b:80/;
    }
}

Dan berkas compose.yaml merangkai ketiganya:

yaml
name: pintu

services:
  proxy:
    image: nginx:1.29-alpine
    ports:
      - "8080:80"
    configs:
      - source: pintu-conf
        target: /etc/nginx/conf.d/default.conf
    depends_on:
      - web-a
      - web-b

  web-a:
    image: nginx:1.29-alpine
    volumes:
      - ./web-a:/usr/share/nginx/html:ro

  web-b:
    image: nginx:1.29-alpine
    volumes:
      - ./web-b:/usr/share/nginx/html:ro

configs:
  pintu-conf:
    file: ./pintu.conf

Mari kita telaah bagian-bagian yang bekerja sama di atas:

  1. configs dengan source dan target — pola yang persis kita kuasai di Chapter Config dan Secret: berkas pintu.conf di host dipasang read-only sebagai /etc/nginx/conf.d/default.conf di dalam container proxy, menggantikan berkas bawaan image nginx.
  2. depends_on bentuk daftar — proxy dinyalakan setelah web-a dan web-b berjalan; alasan mendalam bentuk daftar ini kita bedah di bagian NOTE menjelang akhir chapter.
  3. Bind mount untuk halaman — dua folder html masing-masing menempel di web-a dan web-b secara read-only, pola :ro dari Chapter Mount Read-Only; tidak ada image yang dibangun, cukup image siap pakai.
  4. Hanya proxy yang punya ports — dua backend tidak menerbitkan apa pun; mereka dihubungi proxy lewat port internal container.

Perilaku Trailing Slash pada proxy_pass #

Baris paling menentukan di pintu.conf adalah tanda garis miring di ujung alamat tujuan proxy_pass menuju web-a:80. Aturannya: saat proxy_pass memuat URI — sekecil apa pun, termasuk sekadar / — nginx mengganti bagian jalur yang cocok dengan location menggunakan URI tersebut. Permintaan /web-a/ cocok dengan location /web-a/; bagian yang cocok itu diganti /, sehingga web-a menerima permintaan / — dan menjawab index.html. Begitu pula /web-a/halaman.html akan tiba di backend sebagai /halaman.html: awalan /web-a/ terhapus.

Tanpa trailing slash, perilakunya berubah drastis. Konfigurasi berikut terlihat hampir sama:

conf
    location /web-a/ {
        proxy_pass http://web-a:80;
    }

Kini proxy_pass tidak memuat URI, sehingga nginx meneruskan jalur permintaan apa adanya: /web-a/ tiba di web-a masih sebagai /web-a/. Container web-a mencari berkas di /usr/share/nginx/html/web-a/ — folder yang tidak pernah kita buat — dan menjawab 404:

bash
$ curl -i localhost:8080/web-a/

Output yang Dihasilkan (dipotong):

text
HTTP/1.1 404 Not Found
Server: nginx/1.29.4
Date: Wed, 14 Oct 2026 04:12:01 GMT
Content-Type: text/html

<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.29.4</center>
</body>
</html>

Karena itu konfigurasi pintu tetap memakai trailing slash: hampir setiap kali path publik memakai awalan (/web-a/, /api/), service di belakangnya ingin menerima jalur tanpa awalan itu. Aturan praktisnya sederhana — awalan path di location hampir selalu berarti “awalan yang harus dibuang”, dan trailing slash di proxy_pass adalah alat pembuangnya.

Menjalankan pintu #

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 7/7
 ✔ Network pintu_default    Created    0.2s
 ✔ Container pintu-web-a-1  Created    0.1s
 ✔ Container pintu-web-a-1  Started    0.4s
 ✔ Container pintu-web-b-1  Created    0.1s
 ✔ Container pintu-web-b-1  Started    0.3s
 ✔ Container pintu-proxy-1  Created    0.0s
 ✔ Container pintu-proxy-1  Started    0.6s

Dua jalur diuji bergantian, masing-masing menjawab dengan halaman milik backendnya:

bash
$ curl localhost:8080/web-a/

Output yang Dihasilkan:

text
<h1>Halo dari web-a</h1>
bash
$ curl localhost:8080/web-b/

Output yang Dihasilkan:

text
<h1>Halo dari web-b</h1>

Satu port host, dua service di belakangnya, jalur yang memutuskan arah — mekanisme inti reverse proxy sudah bekerja. Karena toko juga akan memakai port 8080, pintu dibongkar sampai bersih sebelum kita lanjut:

bash
$ docker compose down

Output yang Dihasilkan:

text
[+] Running 4/4
 ✔ Container pintu-proxy-1  Removed    0.4s
 ✔ Container pintu-web-b-1  Removed    0.2s
 ✔ Container pintu-web-a-1  Removed    0.2s
 ✔ Network pintu_default    Removed    0.3s

Satu Pintu untuk Toko #

Sekarang panggung yang sesungguhnya. Folder toko bertambah dua folder html dan dua berkas konfigurasi:

text
toko/
├── compose.yaml
├── nginx.conf
├── backend.conf
├── frontend-html/
│   └── index.html
└── backend-html/
    └── produk.json

Halaman toko sederhana tapi kini memuat satu tautan yang menunjuk ke jalur API tunggal:

html
<!DOCTYPE html>
<html lang="id">
<head><meta charset="utf-8"><title>Toko Kopi Nusantara</title></head>
<body>
  <h1>Toko Kopi Nusantara</h1>
  <p>Daftar produk tersedia di <a href="/api/produk.json">/api/produk.json</a>.</p>
</body>
</html>

Sementara itu backend sementara — masih nginx statis — menunggu giliran menjadi aplikasi sungguhan di chapter berikutnya:

json
[
  {"id": 1, "nama": "Kopi Arabika Gayo", "harga": 85000},
  {"id": 2, "nama": "Teh Melati Putih", "harga": 120000},
  {"id": 3, "nama": "Gula Aren Cair", "harga": 45000}
]

Konfigurasi proxy dan kontrak port 3000 #

Keputusan kecil di sini menghemat pekerjaan besar nanti: backend sementara mendengarkan port 3000, bukan 80 milik nginx. Port 3000 adalah kontrak backend toko — begitu chapter berikutnya menggantinya dengan aplikasi Node.js yang juga mendengarkan 3000, proxy tidak berubah satu karakter pun. Cukup dua baris konfigurasi untuk memindahkan port dengar nginx:

conf
server {
    listen 3000;
}

Berkas nginx.conf milik proxy kemudian berdiri di atas kontrak itu:

conf
server {
    listen 80;

    location /api/ {
        proxy_pass http://backend:3000/;
        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 / {
        proxy_pass http://frontend:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Mari kita telaah konfigurasi tersebut baris demi baris:

  1. listen 80 — proxy mendengarkan port 80 di dalam container-nya; port inilah yang diterbitkan ke host sebagai 8080.
  2. location /api/ dan location / — nginx memilih blok dengan awalan jalur terpanjang yang cocok: permintaan /api/produk.json jatuh ke blok pertama, sedangkan sisanya — termasuk / — jatuh ke blok kedua.
  3. proxy_pass menuju alamat backend:3000 yang diakhiri garis miring — trailing slash membuang awalan /api/: permintaan /api/produk.json tiba di backend sebagai /produk.json. Nama backend dipecahkan DNS network, bukan alamat IP.
  4. proxy_pass menuju alamat frontend:80 tanpa URI di ujung — karena location / tidak memiliki awalan yang perlu dibuang; jalur diteruskan apa adanya.
  5. Tiga proxy_set_header — identitas klien asli yang dibawa setiap permintaan, dibedah setelah compose.yaml di bawah.

compose.yaml dengan Dua Network #

yaml
name: toko

services:
  proxy:
    image: nginx:1.29-alpine
    ports:
      - "8080:80"
    networks:
      - frontend-net
    configs:
      - source: nginx-conf
        target: /etc/nginx/conf.d/default.conf
    depends_on:
      - frontend
      - backend

  frontend:
    image: nginx:1.29-alpine
    volumes:
      - ./frontend-html:/usr/share/nginx/html:ro
    networks:
      - frontend-net

  backend:
    image: nginx:1.29-alpine
    volumes:
      - ./backend-html:/usr/share/nginx/html:ro
    configs:
      - source: backend-conf
        target: /etc/nginx/conf.d/default.conf
    networks:
      - frontend-net
      - backend-net

networks:
  frontend-net:
  backend-net:

configs:
  nginx-conf:
    file: ./nginx.conf
  backend-conf:
    file: ./backend.conf

Mari kita telaah keputusan-keputusan pentingnya:

  1. ports hanya ada pada proxy — frontend dan backend murni internal; klien tidak punya jalan lain selain pintu 8080, dan begitu pula penyerang dari luar.
  2. Tiga penumpang di frontend-net, backend satu-satunya jembatan ke backend-net — topologi berlapis dari chapter sebelumnya kini hidup di deklarasi; backend-net masih sepi karena db baru datang di chapter berikutnya, tetapi jalurnya sudah disiapkan.
  3. configs untuk dua berkas — nginx-conf untuk proxy dan backend-conf untuk backend sementara; keduanya objek project, bukan bind mount yang path-nya tercecer.
  4. depends_on proxy memakai bentuk daftar — cukup urutan start, bukan gerbang kesehatan; alasannya di NOTE di bawah.

Menyalakan Stack dan Menguji Dua Jalur #

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 5/5
 ✔ Network toko_frontend-net    Created    0.2s
 ✔ Network toko_backend-net     Created    0.2s
 ✔ Container toko-backend-1     Started    0.4s
 ✔ Container toko-frontend-1    Started    0.3s
 ✔ Container toko-proxy-1       Started    0.6s

Dua network dan tiga container — lima baris, satu stack. Kedua jalur diuji dari pintu yang sama:

bash
$ curl localhost:8080/

Output yang Dihasilkan:

text
<!DOCTYPE html>
<html lang="id">
<head><meta charset="utf-8"><title>Toko Kopi Nusantara</title></head>
<body>
  <h1>Toko Kopi Nusantara</h1>
  <p>Daftar produk tersedia di <a href="/api/produk.json">/api/produk.json</a>.</p>
</body>
</html>
bash
$ curl localhost:8080/api/produk.json

Output yang Dihasilkan:

text
[
  {"id": 1, "nama": "Kopi Arabika Gayo", "harga": 85000},
  {"id": 2, "nama": "Teh Melati Putih", "harga": 120000},
  {"id": 3, "nama": "Gula Aren Cair", "harga": 45000}
]

Permintaan pertama menempuh jalur / menuju frontend; permintaan kedua menempuh /api/produk.json, kehilangan awalannya di proxy, dan tiba di backend sebagai /produk.json — berkas json statis pun terjawab. Masalah dua pintu Budi kini tinggal kenangan: satu asal, tanpa CORS, satu port untuk firewall, dan satu tempat bagi TLS kelak.

Pola yang lebih dinamis tersedia bagi yang ingin menghapus restart manual itu. nginx bisa diarahkan menyelesaikan nama saat permintaan berjalan, dengan interval cache resolusi tersendiri:

conf
    location /api/ {
        resolver 127.0.0.11 valid=10s;
        set $upstream_api http://backend:3000;
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass $upstream_api;
    }

Mari kita telaah tiga baris barunya: resolver 127.0.0.11 valid=10s menunjuk embedded DNS milik network — alamat yang kita bedah di Chapter Container DNS — dan menyuruh nginx menanyakan ulang setiap 10 detik; set $upstream_api memindahkan alamat upstream ke variabel, yang membuat nginx menunda resolusi sampai permintaan datang; dan rewrite mengambil alih tugas membuang awalan /api/, karena pemetaan jalur otomatis tidak berlaku lagi begitu proxy_pass memuat variabel.

Manfaat yang Konkret bagi Toko #

ManfaatWujudnya pada toko
Satu pintu publikhanya proxy yang memuat ports — 8080 — dan firewall cukup membuka satu port
Routing berbasis jalur/ ke frontend, /api/ ke backend, tanpa satu pun service tahu jalur service lain
Identitas klien utuhbackend menerima Host, X-Real-IP, dan X-Forwarded-For dari proxy
CORS lenyaphalaman dan data kini sesama asal — satu host, satu port
Skalabilitas tersembunyibackend boleh menjadi tiga replika tanpa klien atau browser tahu
TLS kelaksertifikat cukup dipasang di proxy, sebelum lalu lintas terbelah

Tiga header yang diteruskan proxy layak dibedah tuntas karena backend kita akan bergantung padanya. proxy_set_header Host $host mengirim ulang nama host yang diminta klien; tanpa baris ini, backend menerima Host: backend — nama upstream yang ditulis proxy — sehingga aplikasi yang memvirtual-host berdasarkan nama akan menjawab dengan konfigurasi yang salah. proxy_set_header X-Real-IP $remote_addr membawa satu alamat IP klien asli persis seperti dilihat proxy, menggantikan fakta bahwa dari sisi backend semua permintaan tampak datang dari alamat container proxy. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for membawa rantai alamat: nilai $proxy_add_x_forwarded_for adalah daftar X-Forwarded-For yang sudah ada (bila permintaan melewati proxy lain) ditambah alamat klien terakhir — standar de facto yang dipatuhi rantai proxy mana pun, sehingga backend selalu bisa membaca klien paling hulu dari satu tempat.

Toko kini punya wajah: satu pintu, dua jalur, tiga service yang saling menjaga jarak. Tetapi isi pintu /api/ masih tipis — berkas json statis yang tidak berubah sejak ditulis. Chapter Application dan Database mengganti backend sementara itu dengan aplikasi Node.js sungguhan, menyambungkannya ke PostgreSQL yang menyimpan data toko, dan menghadirkan dua keterampilan yang menyertainya: migrasi skema di dalam stack dan connection pool yang menghormati batas database.

Referensi #