Banyak engineer yang pertama kali setup nginx reverse proxy berakhir dengan copy-paste config dari Stack Overflow, lalu bingung kenapa response header-nya aneh atau WebSocket-nya putus setiap 60 detik. Saya pernah di posisi itu — dan jujur, dokumentasi resmi nginx tidak terlalu membantu kalau kamu belum tahu harus cari apa.
Nginx reverse proxy configuration bukan hal yang sulit secara konsep, tapi detailnya bisa menjebak. Salah satu direktif yang terlewat bisa bikin upstream service kamu tidak bisa baca IP asli client, atau SSL termination-nya bocor header yang seharusnya tidak keluar.
Artikel ini fokus ke setup yang saya pakai sendiri di lingkungan self-hosted: mulai dari blok server paling dasar, header yang wajib ada, sampai konfigurasi untuk WebSocket dan SSL termination dengan Let's Encrypt. Semua contoh diuji di nginx reverse proxy setup Ubuntu 22.04 dengan nginx 1.24.x di Ubuntu 22.04.
Anatomi Blok Server yang Benar
Sebelum masuk ke reverse proxy, kamu harus paham struktur dasar dulu. Banyak tutorial langsung loncat ke proxy_pass tanpa jelasin konteksnya.
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
Ini config minimum yang layak. Kenapa proxy_http_version 1.1? Karena default nginx pakai HTTP/1.0 ke upstream, yang tidak support keep-alive. Kalau upstream kamu Node.js atau Go, koneksi baru dibuka setiap request — lambat dan boros.
Empat header di atas bukan hiasan. X-Real-IP dan X-Forwarded-For dibutuhkan upstream service supaya bisa baca IP asli client. Tanpa ini, semua log di aplikasi kamu akan menunjuk ke 127.0.0.1. X-Forwarded-Proto penting untuk aplikasi yang perlu tahu apakah request datang via HTTP atau HTTPS — misalnya untuk redirect logic.
Nginx Reverse Proxy Configuration untuk SSL Termination
Setup paling umum di self-hosted environment: nginx handle SSL, upstream service jalan di HTTP biasa di localhost. Ini yang saya rekomendasikan — lebih simpel dan upstream service tidak perlu tahu soal sertifikat.
Asumsi kamu sudah punya sertifikat dari Let's Encrypt via certbot:
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
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;
# Rekomendasi Mozilla Intermediate compatibility
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
}
}
Blok pertama redirect semua HTTP ke HTTPS. Jangan skip ini — bukan soal SEO, tapi soal tidak sengaja ngirim credentials via plain HTTP.
Satu hal yang sering dilupakan: ssl_session_cache. Tanpa ini, setiap TLS handshake butuh full negotiation. Dengan shared:SSL:10m, session bisa di-resume selama 1 hari. Ini terasa di aplikasi yang banyak koneksi pendek.
Konfigurasi WebSocket
WebSocket butuh perlakuan khusus karena protokolnya berbeda — dimulai sebagai HTTP upgrade request. Config standar di atas tidak cukup.
server {
listen 443 ssl;
server_name app.example.com;
# ... ssl config sama seperti di atas ...
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Wajib untuk WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
# Timeout lebih panjang untuk koneksi WebSocket yang idle
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Dua header Upgrade dan Connection itu yang bikin HTTP connection bisa di-upgrade ke WebSocket. Tanpa keduanya, nginx akan drop upgrade request dan client dapat error 400 atau koneksi langsung putus.
proxy_read_timeout default nginx adalah 60 detik. Untuk WebSocket yang mungkin idle lama (chat app, live dashboard), naikkan ke 3600s atau sesuai kebutuhan. Kalau tidak, nginx akan close koneksi yang dianggap idle terlalu lama.
Load Balancing ke Multiple Upstream
Kalau kamu jalankan lebih dari satu instance aplikasi — misalnya beberapa container Docker — nginx bisa distribute traffic ke semua instance. Ini juga berguna untuk zero-downtime deployment sederhana.
upstream app_backend {
# Default: round-robin
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
# Kalau mau sticky session berdasarkan IP
# ip_hash;
keepalive 32;
}
server {
listen 443 ssl;
server_name app.example.com;
# ... ssl config ...
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
}
}
Perhatikan keepalive 32 di blok upstream dan proxy_set_header Connection "" di blok location. Kombinasi ini yang mengaktifkan HTTP keep-alive ke upstream. Kalau salah satu hilang, keep-alive tidak akan bekerja meski kamu sudah set proxy_http_version 1.1.
Default load balancing nginx adalah round-robin. Cukup untuk kebanyakan kasus. ip_hash berguna kalau aplikasi kamu punya session state di memory dan belum pakai shared session store — tapi ini bukan solusi jangka panjang.
Buffering, Timeout, dan Ukuran Request
Ini bagian yang paling sering bikin masalah di production tapi jarang dibahas tutorial.
server {
# ... konfigurasi lainnya ...
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Header standar
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;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
# Timeout
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Ukuran request body
client_max_body_size 10m;
}
}
Beberapa poin penting:
Buffering: Kalau proxy_buffering on, nginx baca seluruh response dari upstream dulu sebelum kirim ke client. Ini bagus untuk upstream yang lambat, tapi bisa jadi masalah untuk streaming response (SSE, chunked transfer). Kalau kamu punya endpoint streaming, matikan buffering untuk location spesifik itu: proxy_buffering off.
client_max_body_size: Default nginx adalah 1MB. Kalau aplikasi kamu terima file upload, naikkan ini. Error 413 (Request Entity Too Large) hampir selalu karena setting ini.
proxy_connect_timeout: Berapa lama nginx tunggu upstream menerima koneksi. 10 detik sudah cukup panjang — kalau upstream tidak respond dalam 10 detik, ada masalah yang lebih serius.
Perbandingan: HTTP vs HTTPS vs WebSocket Config
| Aspek | HTTP Basic | HTTPS + SSL Termination | WebSocket |
|---|---|---|---|
listen |
80 |
443 ssl |
443 ssl |
proxy_http_version |
Opsional | 1.1 (wajib) |
1.1 (wajib) |
| Header Upgrade | Tidak perlu | Tidak perlu | Wajib |
proxy_read_timeout |
Default 60s | Default 60s | Naikkan ke 3600s |
| SSL certificate | Tidak ada | Wajib | Wajib |
| Cocok untuk | Dev/internal | Semua production | Real-time app |
Tips Debugging Config yang Salah
Kalau setelah setup kamu masih dapat masalah, urutan debug yang saya pakai:
1. Test config sebelum reload:
nginx -t
Jangan pernah langsung systemctl reload nginx tanpa ini. Satu syntax error bisa bikin nginx tidak mau reload dan semua request gagal.
2. Cek error log secara real-time:
tail -f /var/log/nginx/error.log
Error connect() failed (111: Connection refused) artinya upstream service tidak jalan atau salah port. Error upstream timed out artinya upstream lambat atau tidak respond.
3. Verifikasi header yang diterima upstream: Kalau pakai Node.js, tambahkan sementara:
app.use((req, res, next) => {
console.log(req.headers);
next();
});
Ini cara paling cepat verifikasi apakah X-Real-IP dan header lain sampai dengan benar.
4. Cek apakah nginx baca config yang benar:
nginx -T | grep server_name
Kalau kamu punya banyak virtual host, kadang config yang aktif bukan yang kamu edit.
Satu hal lagi: kalau kamu pakai UFW atau iptables, pastikan port 80 dan 443 sudah dibuka. Saya pernah debug nginx selama 30 menit sebelum sadar firewall-nya yang blokir.
Kesimpulan
Nginx reverse proxy configuration yang solid butuh lebih dari sekadar proxy_pass. Header yang tepat, timeout yang masuk akal, dan handling WebSocket yang benar — semua itu penting untuk setup yang benar-benar bisa diandalkan di production.
Kalau kamu baru mulai, gunakan config SSL termination di atas sebagai baseline. Itu sudah cukup untuk 90% use case self-hosted. Tambahkan WebSocket config kalau aplikasi kamu butuhkan, dan sesuaikan timeout serta buffer size berdasarkan karakteristik traffic kamu.
Apa yang harus kamu lakukan besok? Ambil salah satu service yang kamu expose langsung ke internet, pasang nginx di depannya dengan config dari artikel ini, dan verifikasi header-nya sampai dengan benar ke upstream. Itu saja. Satu service dulu — sisanya bisa menyusul.
Kalau kamu juga mengelola beberapa service sekaligus, baca juga artikel tentang manajemen virtual host nginx untuk multi-service setup — ada beberapa pattern yang bisa menghemat banyak duplikasi config.