Referensi cepat reverse proxy. Routing, TLS/ACME, load balancing, header, dan tuning performa di nginx, Caddy, dan Traefik. Perfect buat DevOps.
Ketiga tool ini menyelesaikan masalah yang sama (terima trafik masuk, teruskan ke backend) dengan filosofi konfigurasi yang beda jauh. Pilih berdasarkan situasimu, bukan popularitas.
| Aspek | nginx | Caddy | Traefik |
|---|---|---|---|
| Gaya config | file statis manual | Caddyfile singkat | label Docker + file dynamic |
| TLS/ACME | lewat certbot eksternal | otomatis bawaan | otomatis via certificatesResolver |
| Service discovery | manual (edit upstream) | manual / DNS | otomatis dari Docker, Kubernetes |
| Binary size | kecil, ada di mana-mana | satu binary Go | satu binary Go |
| Cocok untuk | tim yang butuh kontrol penuh | VPS kecil, setup cepat | lingkungan container yang berubah cepat |
Panduan kasar: nginx kalau kamu butuh kontrol detail dan tim sudah paham sintaksnya. Caddy kalau kamu mau HTTPS jalan lima menit setelah install. Traefik kalau service-mu hidup di Docker dan skala container berubah tiap hari.
Reverse proxy duduk di depan backend dan jadi satu-satunya pintu masuk dari internet. Yang bisa dia lakukan:
api.example.com ke service A, path /admin ke service BInternet -> :443 proxy -> app:3000
-> api:4000
-> admin:5000Semua contoh di bawah berkaitan erat dengan container. Kalau kamu belum paham docker compose, baca dulu cheat sheet Docker Compose.
Routing nginx hidup di server block (per domain) dan location block (per path). Urutan pencocokan location:
= exact match, menang kalau ada^~ prefix match terpanjang, kalau cocok maka regex dilewati~ dan ~* regex (case sensitive dan tidak), dicek berurutanserver {
listen 80;
server_name app.example.com;
# path diteruskan apa adanya ke upstream
location / {
proxy_pass http://127.0.0.1:3000;
}
# perhatikan slash di akhir proxy_pass: /api/users jadi /users
location /api/ {
proxy_pass http://127.0.0.1:4000/;
}
location = /healthz {
return 200 "ok\n";
access_log off;
}
}Satu slash di akhir proxy_pass mengubah path yang dikirim ke backend. Ini sumber bug klasik yang bikin 404 misterius, jadi hati-hati ya.
nginx tidak punya ACME client bawaan, jadi pakai certbot untuk urusan sertifikat. certbot bisa mode --nginx (baca config langsung) atau webroot.
sudo certbot --nginx -d app.example.com -d www.app.example.com
sudo certbot renew --dry-runPembaruan otomatis lewat systemd timer. Setelah renew, nginx perlu reload, tambahkan deploy hook:
sudo certbot renew --deploy-hook "systemctl reload nginx"Config HTTPS lengkap dengan redirect:
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
location / {
proxy_pass http://127.0.0.1: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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Definisikan pool backend di block upstream, lalu arahkan proxy_pass ke nama pool itu.
upstream app {
least_conn; # algoritma: pilih koneksi tersedikit
server 10.0.0.11:3000 weight=2; # dapat 2x porsi trafik
server 10.0.0.12:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.13:3000 backup; # cuma dipakai kalau semua utama mati
keepalive 32; # pool koneksi idle per worker
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/certs/app.pem;
ssl_certificate_key /etc/nginx/certs/app.key;
location / {
proxy_pass http://app;
proxy_http_version 1.1; # wajib untuk keepalive upstream
proxy_set_header Connection ""; # wajib juga, hapus header Connection
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Opsi algoritma yang tersedia:
| Direktif | Perilaku |
|---|---|
| default (round robin) | bergiliran, dikali weight |
least_conn | server dengan koneksi aktif paling sedikit |
ip_hash | IP klien yang sama selalu ke server yang sama (sticky tanpa cookie) |
hash $request_uri | hash berdasarkan URI, cocok untuk cache hit ratio |
random two least_conn | pilih 2 kandidat acak lalu ambil yang paling kosong, murah untuk cluster besar |
Catatan penting: keepalive upstream cuma jalan kalau kamu set proxy_http_version 1.1; dan bersihkan header Connection. Tanpa dua baris itu, nginx buka koneksi TCP baru untuk tiap request dan performanya turun drastis.
Keunggulan Caddy: HTTPS otomatis. Dia minta sertifikat via ACME (Let's Encrypt dan ZeroSSL) saat start, memperbarui sendiri sebelum kedaluwarsa, dan redirect HTTP ke HTTPS tanpa kamu tulis apa pun.
app.example.com {
encode zstd gzip
reverse_proxy localhost:3000
}Untuk domain yang tidak bisa lewat ACME (local dev, internal):
localhost {
tls internal
reverse_proxy localhost:3000
}Routing berdasarkan path pakai handle. handle_path sekalian membuang prefixnya, jadi /api/users tiba di backend sebagai /users.
app.example.com {
@assets path /assets/*
handle @assets {
root * /srv/assets
file_server
}
handle_path /api/* {
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:3000
}
}Block paling atas (global) untuk akun email ACME:
{
email admin@example.com
}app.example.com {
reverse_proxy 10.0.0.11:3000 10.0.0.12:3000 10.0.0.13:3000 {
lb_policy round_robin
health_uri /health
health_interval 10s
health_timeout 2s
fail_duration 30s
header_up X-Real-IP {remote_host}
}
}lb_policy yang sering dipakai: random (default), round_robin, first, least_conn, ip_hash, header, dan cookie (sticky session, cocok kalau session kamu disimpan di memori instance). Health check aktif (health_uri) mendeteksi backend bermasalah dalam hitungan detik, sedangkan fail_duration dan max_fails adalah health check pasif yang mengandalkan kegagalan request nyata.
Defaultnya Caddy tidak percaya header X-Forwarded-For dari siapa pun. Kalau ada proxy lain di depan Caddy (misal CDN), tambahkan:
reverse_proxy localhost:3000 {
trusted_proxies private_ranges
}Traefik memisahkan dua jenis config. Statis (entryPoints, providers, certificatesResolvers) dibaca sekali saat start, biasanya lewat file traefik.yml atau CLI flags. Dinamis (routers, services, middlewares) bisa berubah kapan saja lewat label container atau file di directory provider.
Statis, simpan sebagai traefik.yml:
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
exposedByDefault: false
network: proxy
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
api:
dashboard: trueDinamis, cukup label di docker-compose.yml:
services:
traefik:
image: traefik:v3.4
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks: [proxy]
app:
image: myapp:1.4.2
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
api:
image: myapi:2.1.0
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`) && PathPrefix(`/v1`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.services.api.loadbalancer.server.port=4000"
- "traefik.http.middlewares.api-strip.stripprefix.prefixes=/v1"
- "traefik.http.routers.api.middlewares=api-strip@docker"
networks:
proxy:
name: proxyBagian terbaiknya: docker compose up -d --scale api=3 langsung jadi load balancer ke 3 container tanpa config tambahan. Traefik melihat container baru lewat Docker API dan menambahkannya ke pool.
File acme.json harus permission 600 atau Traefik menolak menyimpan sertifikat. Ini error pertama yang hampir semua orang alami kok.
Middleware memproses request sebelum sampai ke service. Beberapa yang paling sering dipakai:
- "traefik.http.middlewares.limit.ratelimit.average=100"
- "traefik.http.middlewares.limit.ratelimit.burst=50"
- "traefik.http.middlewares.secure.headers.stsseconds=31536000"
- "traefik.http.middlewares.secure.headers.contenttypenosniff=true"
- "traefik.http.middlewares.compress.compress=true"
- "traefik.http.services.api.loadbalancer.sticky.cookie=true"
- "traefik.http.services.api.loadbalancer.healthcheck.path=/health"Urutan middleware di router menentukan urutan eksekusi, jadi taruh rate limit sebelum hal mahal seperti kompresi kalau kamu ingin blokir dulu.
Header berikut adalah kontrak antara proxy dan aplikasi. Backend yang benar membacanya, bukan mengira IP klien adalah IP proxy.
| Header | Isi | Kegunaan |
|---|---|---|
X-Forwarded-For | rantai IP klien, ditambah tiap hop proxy | audit, rate limit per IP |
X-Real-IP | IP klien terakhir yang dipercaya proxy | log aplikasi |
X-Forwarded-Proto | http atau https | generate URL yang benar |
X-Forwarded-Host | host asli yang diminta klien | virtual host di backend |
Host | domain asli dari request | routing dan CSRF check |
Peringatan keamanan: klien bisa mengirim X-Forwarded-For palsu. Proxy yang benar menimpa atau menambah di posisi yang tepat, dan aplikasi hanya boleh percaya hop terakhir sebelum proxy tepercaya. Kalau ada CDN di depan nginx, aktifkan modul realip supaya $remote_addr berisi IP asli:
# contoh untuk Cloudflare, sesuaikan dengan rentang IP CDN-mu
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
real_ip_header X-Forwarded-For;
real_ip_recursive on;WebSocket perlu header upgrade eksplisit di nginx:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location /ws/ {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}Hal dengan dampak terbesar, urut dari yang paling sering dilupakan:
keepalive 32 plus dua baris wajib di atas. Traefik: atur di serversTransport (forwardingTimeouts.idleConnTimeout). Caddy sudah reuse koneksi secara default.gzip on; gzip_types text/css application/json;. Caddy: encode zstd gzip. Traefik: middleware compress. Ukuran payload JSON turun 70 persen lebih, gratis.proxy_buffering off; di nginx dan flush_interval -1 di Caddy.proxy_read_timeout default 60 detik di nginx. Untuk endpoint yang lambat, naikkan khusus di location itu, jangan global.worker_processes auto; dan worker_connections 4096; di block events. Kalau kamu lihat error "too many open files", naikkan limit file descriptor juga.http2 on di nginx (versi 1.25.1 ke atas memakai sintaks ini, versi lama pakai listen 443 ssl http2;). Caddy mendukung HTTP/3 dan mengaktifkannya sejak versi 2.6.client_max_body_size 20m; di nginx supaya upload raksasa tidak menghabiskan memori.Reload tanpa downtime: nginx -t && nginx -s reload (worker lama menyelesaikan request yang sedang jalan), caddy reload via API admin, dan Traefik hot reload setiap kali dynamic config berubah. Prinsipnya sama: validasi dulu, reload kemudian.
nginx -t # uji config sebelum reload
tail -f /var/log/nginx/error.log # 502 biasanya backend mati atau salah port
caddy validate --config /etc/caddy/Caddyfile
caddy adapt --config Caddyfile --adapter caddyfile # lihat JSON final hasil adaptasi
docker logs traefik # tambahkan --log.level=DEBUG untuk detail router
curl -H "Host: app.example.com" http://127.0.0.1/ # tes routing tanpa perlu DNS
curl -I https://app.example.com # cek header respons dan sertifikatPola error cepat: 502 Bad Gateway berarti proxy tidak bisa bicara dengan backend (cek port, cek container jalan di network yang sama). 504 Gateway Timeout berarti backend merespons terlalu lambat dibanding timeout. Redirect loop biasanya X-Forwarded-Proto tidak diteruskan dan aplikasi mengira request masih HTTP.
X-Forwarded-* diteruskan dan aplikasi membacanyanginx -t, caddy validate) sebelum reload via CILogin atau daftar akun gratis untuk membaca cheat sheet ini.