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:
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.
| Aspek | Forward proxy | Reverse proxy |
|---|---|---|
| Berdiri di sisi | klien | server |
| Yang mengonfigurasinya | klien — browser atau sistem kantor | operator aplikasi |
| Yang tidak sadar keberadaannya | server tujuan | klien |
| Menyembunyikan | identitas klien dari server | topologi server dari klien |
| Contoh | proxy penyaring trafik keluar kantor | nginx di depan aplikasi |
Lima Manfaat Reverse Proxy #
| Manfaat | Penjelasan singkat |
|---|---|
| Satu pintu publik | hanya satu port host yang diterbitkan; service lain murni internal |
| TLS di satu tempat | sertifikat dipasang dan diperbarui di proxy saja, bukan di tiap service |
| Routing berbasis jalur | / dan /api/ bisa diarahkan ke service berbeda lewat konfigurasi |
| Topologi tersembunyi | jumlah, nama, dan alamat service internal tidak terpapar ke luar |
| Log dan header terpusat | seluruh 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:
pintu/
├── compose.yaml
├── pintu.conf
├── web-a/
│ └── index.html
└── web-b/
└── index.htmlDua halaman mini menjadi isi backend:
<h1>Halo dari web-a</h1><h1>Halo dari web-b</h1>Konfigurasi proxy-nya — berkas pintu.conf — adalah reverse proxy terkecil yang masih berguna:
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:
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.confMari kita telaah bagian-bagian yang bekerja sama di atas:
configsdengansourcedantarget— pola yang persis kita kuasai di Chapter Config dan Secret: berkaspintu.confdi host dipasang read-only sebagai/etc/nginx/conf.d/default.confdi dalam container proxy, menggantikan berkas bawaan image nginx.depends_onbentuk daftar — proxy dinyalakan setelahweb-adanweb-bberjalan; alasan mendalam bentuk daftar ini kita bedah di bagian NOTE menjelang akhir chapter.- Bind mount untuk halaman — dua folder html masing-masing menempel di
web-adanweb-bsecara read-only, pola:rodari Chapter Mount Read-Only; tidak ada image yang dibangun, cukup image siap pakai. - 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:
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:
$ curl -i localhost:8080/web-a/Output yang Dihasilkan (dipotong):
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 #
$ docker compose up -dOutput yang Dihasilkan:
[+] 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.6sDua jalur diuji bergantian, masing-masing menjawab dengan halaman milik backendnya:
$ curl localhost:8080/web-a/Output yang Dihasilkan:
<h1>Halo dari web-a</h1>$ curl localhost:8080/web-b/Output yang Dihasilkan:
<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:
$ docker compose downOutput yang Dihasilkan:
[+] 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.3sSatu Pintu untuk Toko #
Sekarang panggung yang sesungguhnya. Folder toko bertambah dua folder html dan dua berkas konfigurasi:
toko/
├── compose.yaml
├── nginx.conf
├── backend.conf
├── frontend-html/
│ └── index.html
└── backend-html/
└── produk.jsonHalaman toko sederhana tapi kini memuat satu tautan yang menunjuk ke jalur API tunggal:
<!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:
[
{"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:
server {
listen 3000;
}Berkas nginx.conf milik proxy kemudian berdiri di atas kontrak itu:
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:
listen 80— proxy mendengarkan port 80 di dalam container-nya; port inilah yang diterbitkan ke host sebagai 8080.location /api/danlocation /— nginx memilih blok dengan awalan jalur terpanjang yang cocok: permintaan/api/produk.jsonjatuh ke blok pertama, sedangkan sisanya — termasuk/— jatuh ke blok kedua.proxy_passmenuju alamatbackend:3000yang diakhiri garis miring — trailing slash membuang awalan/api/: permintaan/api/produk.jsontiba di backend sebagai/produk.json. Namabackenddipecahkan DNS network, bukan alamat IP.proxy_passmenuju alamatfrontend:80tanpa URI di ujung — karenalocation /tidak memiliki awalan yang perlu dibuang; jalur diteruskan apa adanya.- Tiga
proxy_set_header— identitas klien asli yang dibawa setiap permintaan, dibedah setelah compose.yaml di bawah.
compose.yaml dengan Dua Network #
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.confMari kita telaah keputusan-keputusan pentingnya:
portshanya ada pada proxy — frontend dan backend murni internal; klien tidak punya jalan lain selain pintu 8080, dan begitu pula penyerang dari luar.- Tiga penumpang di
frontend-net, backend satu-satunya jembatan kebackend-net— topologi berlapis dari chapter sebelumnya kini hidup di deklarasi;backend-netmasih sepi karena db baru datang di chapter berikutnya, tetapi jalurnya sudah disiapkan. configsuntuk dua berkas —nginx-confuntuk proxy danbackend-confuntuk backend sementara; keduanya objek project, bukan bind mount yang path-nya tercecer.depends_onproxy memakai bentuk daftar — cukup urutan start, bukan gerbang kesehatan; alasannya di NOTE di bawah.
Menyalakan Stack dan Menguji Dua Jalur #
$ docker compose up -dOutput yang Dihasilkan:
[+] 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.6sDua network dan tiga container — lima baris, satu stack. Kedua jalur diuji dari pintu yang sama:
$ curl localhost:8080/Output yang Dihasilkan:
<!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>$ curl localhost:8080/api/produk.jsonOutput yang Dihasilkan:
[
{"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:
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 #
| Manfaat | Wujudnya pada toko |
|---|---|
| Satu pintu publik | hanya 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 utuh | backend menerima Host, X-Real-IP, dan X-Forwarded-For dari proxy |
| CORS lenyap | halaman dan data kini sesama asal — satu host, satu port |
| Skalabilitas tersembunyi | backend boleh menjadi tiga replika tanpa klien atau browser tahu |
| TLS kelak | sertifikat 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 #
- nginx Documentation
— directive
proxy_pass,proxy_set_header, danresolverbeserta perilaku trailing slash. - Configs — Compose File Reference — pemasangan berkas konfigurasi ke service sebagai objek project.
- nginx — Docker Hub
— image nginx alpine dan pemuatan berkas dari direktori
conf.d.