proxy_pass

proxy_pass #

proxy_pass adalah directive inti dari reverse proxy Nginx. Ia menentukan ke mana request harus diteruskan. Meskipun terlihat sederhana, ada satu detail — trailing slash — yang bisa menyebabkan bug halus yang sangat sulit di-debug. Artikel ini akan membedah semua aspeknya sampai tuntas.

Penggunaan Paling Dasar #

server {
    listen 443 ssl;
    server_name example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Setiap request ke example.com akan diteruskan ke proses yang berjalan di port 3000. Ini yang paling sering kita butuhkan untuk mendeploy aplikasi Node.js, Python, atau framework backend lainnya.


Detail Kritis: Trailing Slash di proxy_pass #

Ini adalah sumber bug paling umum di konfigurasi reverse proxy. Perilaku proxy_pass berubah secara fundamental tergantung ada atau tidaknya URI (termasuk trailing slash) di nilai proxy_pass.

Aturan: Tanpa URI di proxy_pass #

location /api/ {
    proxy_pass http://localhost:3000;
    # Tidak ada URI setelah port — hanya host:port
}

Ketika proxy_pass tidak menyertakan URI, Nginx meneruskan URI request apa adanya ke backend. Tidak ada modifikasi path.

Request: GET /api/users/123
Backend menerima: GET /api/users/123

Aturan: Dengan URI di proxy_pass #

location /api/ {
    proxy_pass http://localhost:3000/;
    # Ada trailing slash — ini adalah URI
}

Ketika proxy_pass menyertakan URI (bahkan hanya /), Nginx menggantikan bagian yang cocok dengan location dengan URI yang ada di proxy_pass.

Request: GET /api/users/123
Nginx: cocokkan /api/ dari URI
       sisa setelah /api/ = users/123
       gabungkan dengan URI di proxy_pass: / + users/123 = /users/123
Backend menerima: GET /users/123

Perbandingan Lengkap dalam Satu Tabel #

KonfigurasiRequestYang Diterima Backend
proxy_pass http://backend di location /api/GET /api/usersGET /api/users
proxy_pass http://backend/ di location /api/GET /api/usersGET /users
proxy_pass http://backend/v2/ di location /api/GET /api/usersGET /v2/users
proxy_pass http://backend/service di location /api/GET /api/usersGET /serviceusers ← BUG!
proxy_pass http://backend/service/ di location /api/GET /api/usersGET /service/users

Baris keempat adalah jebakan paling umum: jika URI di proxy_pass tidak diakhiri / tapi location-nya diakhiri /, path akan digabung tanpa pemisah.

flowchart TD
    REQ["Request: GET /api/users/123"] --> Q{"Ada URI\ndi proxy_pass?"}
    Q -- "Tidak\nproxy_pass http://backend" --> NOURI["Teruskan URI apa adanya\nBackend terima: /api/users/123"]
    Q -- "Ya\nproxy_pass http://backend/" --> WITHURI["Ganti bagian location\ndengan URI di proxy_pass\nBackend terima: /users/123"]
    Q -- "Ya dengan prefix\nproxy_pass http://backend/v2/" --> WPREFIX["Ganti bagian location\ndengan prefix baru\nBackend terima: /v2/users/123"]

    style NOURI fill:none,stroke-width:2px
    style WITHURI fill:none,stroke-width:2px
    style WPREFIX fill:none,stroke-width:2px

Kapan Menggunakan Masing-Masing #

# ─── Tanpa trailing slash: backend expect path lengkap ────────────────────────
# Gunakan ini saat backend dikode untuk menerima path /api/...
location /api/ {
    proxy_pass http://localhost:3000;
    # GET /api/users → backend: GET /api/users (sesuai route di backend)
}

# ─── Dengan trailing slash: strip prefix /api/ ────────────────────────────────
# Gunakan ini saat backend dikode tanpa prefix /api/ di route-nya
location /api/ {
    proxy_pass http://localhost:3000/;
    # GET /api/users → backend: GET /users (prefix /api/ dihapus)
}

# ─── Dengan path prefix: ubah prefix ──────────────────────────────────────────
# Gunakan ini untuk versioning atau path mapping
location /api/ {
    proxy_pass http://localhost:3000/v2/;
    # GET /api/users → backend: GET /v2/users
}

proxy_http_version: Wajib Diubah ke 1.1 #

Nginx secara default menggunakan HTTP/1.0 untuk koneksi ke backend. HTTP/1.0 tidak mendukung keepalive atau chunked transfer encoding. Selalu ubah ke 1.1 untuk aplikasi modern:

location / {
    proxy_pass http://backend;

    # Wajib untuk:
    # - Keepalive connections ke backend
    # - Chunked transfer encoding
    # - WebSocket
    proxy_http_version 1.1;

    # Hapus header Connection dari klien agar tidak mengganggu keepalive
    proxy_set_header Connection "";
}

Proxy ke Unix Socket #

Untuk performa lebih baik pada mesin yang sama, gunakan Unix socket daripada TCP localhost. Unix socket menghindari overhead TCP stack sepenuhnya — tidak ada handshake, tidak ada network stack, komunikasi langsung melalui kernel.

# ─── Aplikasi yang listen di Unix socket ─────────────────────────────────────
location / {
    proxy_pass http://unix:/run/myapp/myapp.sock;
    # Format: http://unix:/path/to/socket
}

# ─── Gunicorn (Python) via Unix socket ───────────────────────────────────────
location / {
    proxy_pass http://unix:/run/gunicorn/gunicorn.sock;
    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;
}

# ─── PHP-FPM via Unix socket (menggunakan fastcgi, bukan proxy) ──────────────
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

Benchmark umum: Unix socket bisa 10-30% lebih cepat dari TCP localhost untuk request kecil yang banyak (seperti API calls). Untuk file besar, perbedaannya tidak signifikan.

Membuat Aplikasi Listen di Unix Socket #

# Node.js: jalankan di Unix socket
node server.js --socket /run/myapp/myapp.sock

# Atau dalam kode:
const app = express();
app.listen('/run/myapp/myapp.sock');

# Gunicorn:
gunicorn --bind unix:/run/gunicorn/gunicorn.sock myapp:app

# Pastikan Nginx worker punya izin baca/tulis ke socket
chown nginx:nginx /run/myapp/myapp.sock
chmod 660 /run/myapp/myapp.sock

WebSocket: Upgrade Koneksi #

WebSocket memerlukan upgrade dari HTTP ke protokol WebSocket. Nginx perlu dikonfigurasi secara khusus untuk meneruskan Upgrade header:

http {
    # Map untuk menentukan nilai Connection header
    # Jika ada Upgrade header → Connection: upgrade
    # Jika tidak ada → Connection: close
    map $http_upgrade $connection_upgrade {
        default  upgrade;
        ''       close;
    }

    server {
        location /ws/ {
            proxy_pass http://localhost:4000;
            proxy_http_version 1.1;

            # Dua header ini wajib untuk WebSocket
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection $connection_upgrade;

            # 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;

            # Timeout panjang untuk koneksi WebSocket yang long-lived
            proxy_read_timeout  3600s;
            proxy_send_timeout  3600s;
        }
    }
}

Bagaimana WebSocket handshake bekerja melalui Nginx:

sequenceDiagram
    participant B as Browser
    participant N as Nginx
    participant WS as WebSocket Server

    B->>N: GET /ws/ HTTP/1.1\nUpgrade: websocket\nConnection: Upgrade
    N->>WS: GET /ws/ HTTP/1.1\nUpgrade: websocket\nConnection: Upgrade
    WS->>N: HTTP/1.1 101 Switching Protocols\nUpgrade: websocket\nConnection: Upgrade
    N->>B: HTTP/1.1 101 Switching Protocols
    Note over B,WS: Koneksi WebSocket terbuka — dua arah
    B->>N: WebSocket Frame
    N->>WS: WebSocket Frame
    WS->>N: WebSocket Frame
    N->>B: WebSocket Frame

Path Rewriting Sebelum Proxy #

Kadang kita perlu mengubah path request sebelum diteruskan ke backend. Ada beberapa cara:

Cara 1: Trailing Slash (Paling Sederhana) #

# /app/users → backend menerima /users (prefix /app/ dihapus)
location /app/ {
    proxy_pass http://localhost:8080/;
}

Cara 2: rewrite dengan break #

# Hapus prefix /api/v1/ sebelum diteruskan
location /api/v1/ {
    rewrite ^/api/v1/(.*) /$1 break;
    # break: hentikan rewrite processing, teruskan dengan URI baru
    proxy_pass http://localhost:3000;

    # /api/v1/users → backend: /users
    # /api/v1/products/123 → backend: /products/123
}

Cara 3: Regex location dengan capture group #

# Ubah /service/v2/endpoint ke /endpoint?version=2
location ~* ^/service/v(\d+)/(.+)$ {
    proxy_pass http://backend/$2?version=$1;
    # /service/v2/users → backend: /users?version=2
}

Cara 4: sub_filter untuk Konten Response #

Kadang backend mengembalikan URL absolut dalam response yang perlu diubah:

location /app/ {
    proxy_pass http://internal-backend/;

    # Ganti semua URL internal dengan URL publik dalam response HTML
    sub_filter 'http://internal-backend' 'https://example.com/app';
    sub_filter_once off;  # Ganti semua kemunculan, bukan hanya pertama
    sub_filter_types text/html text/javascript application/json;
}

Proxy ke Upstream Group #

Dalam praktik production, proxy_pass hampir selalu mengarah ke upstream block daripada alamat tunggal. Ini memungkinkan load balancing dan failover otomatis:

upstream app_backend {
    # Round-robin secara default
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;

    # Pool keepalive connections
    keepalive 32;
}

server {
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Detail upstream dan load balancing dibahas secara mendalam di Section 06.


Template Konfigurasi Production-Ready #

Berikut template konfigurasi reverse proxy yang lengkap dan siap production untuk aplikasi Node.js/Python:

# /etc/nginx/conf.d/myapp.com.conf

# ─── HTTP → HTTPS Redirect ───────────────────────────────────────────────────
server {
    listen 80;
    server_name myapp.com www.myapp.com;
    return 301 https://$host$request_uri;
}

# ─── HTTPS + Reverse Proxy ───────────────────────────────────────────────────
server {
    listen 443 ssl;
    http2  on;
    server_name myapp.com www.myapp.com;

    # SSL
    ssl_certificate     /etc/letsencrypt/live/myapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;

    # Keamanan
    server_tokens off;
    add_header X-Content-Type-Options  "nosniff"    always;
    add_header X-Frame-Options         "SAMEORIGIN" always;
    add_header Referrer-Policy         "strict-origin" always;

    # ─── Static files langsung dari Nginx (lebih cepat dari backend) ─────────
    location /static/ {
        alias /var/www/myapp/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # ─── Favicon dan robots.txt ───────────────────────────────────────────────
    location = /favicon.ico { alias /var/www/myapp/static/favicon.ico; access_log off; }
    location = /robots.txt  { alias /var/www/myapp/static/robots.txt; access_log off; }

    # ─── Aplikasi utama → Node.js backend ────────────────────────────────────
    location / {
        proxy_pass         http://localhost:3000;
        proxy_http_version 1.1;

        # Headers agar backend tahu informasi klien asli
        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;

        # Keepalive
        proxy_set_header Connection "";

        # Timeout
        proxy_connect_timeout  10s;
        proxy_read_timeout     60s;
        proxy_send_timeout     60s;

        # Buffer
        proxy_buffering         on;
        proxy_buffer_size       4k;
        proxy_buffers           16 4k;

        # Error handling
        proxy_intercept_errors  on;
        error_page 502 503 504 /maintenance.html;
    }

    # ─── WebSocket ────────────────────────────────────────────────────────────
    location /ws/ {
        proxy_pass          http://localhost:3001;
        proxy_http_version  1.1;
        proxy_set_header    Upgrade    $http_upgrade;
        proxy_set_header    Connection $connection_upgrade;
        proxy_set_header    Host       $host;
        proxy_read_timeout  3600s;
    }

    # ─── Halaman maintenance ──────────────────────────────────────────────────
    location = /maintenance.html {
        root /var/www/myapp;
        internal;
    }

    # ─── Blokir akses sensitif ────────────────────────────────────────────────
    location ~ /\. { deny all; }
}

Debugging proxy_pass #

# 1. Pastikan backend bisa diakses dari Nginx
curl -v http://localhost:3000/api/health

# 2. Cek apakah Nginx menerima dan meneruskan dengan benar
# Tambahkan header debug sementara
location /api/ {
    proxy_pass http://localhost:3000;
    add_header X-Proxy-By "nginx" always;
    add_header X-Upstream-Addr $upstream_addr always;  # IP backend yang dipilih
}

# 3. Lihat upstream response time di log
log_format proxy_log '$remote_addr - [$time_local] "$request" '
                     '$status $body_bytes_sent '
                     '$upstream_response_time '  # Waktu backend merespons
                     '$upstream_addr';           # Backend yang dipakai

# 4. Cek error log untuk pesan koneksi
tail -f /var/log/nginx/error.log | grep -E "connect|upstream"

# Error umum:
# "connect() failed (111: Connection refused)" → backend tidak berjalan
# "no live upstreams while connecting to upstream" → semua backend down
# "upstream timed out (110: Connection timed out)" → backend terlalu lambat

Proxy ke Beberapa Backend Berdasarkan Kondisi #

Dalam skenario yang lebih kompleks, kita bisa meneruskan request ke backend yang berbeda berdasarkan kondisi tertentu — seperti versi API, tipe perangkat, atau A/B testing:

http {
    # A/B testing: 20% traffic ke versi baru
    split_clients "${remote_addr}${uri}" $app_version {
        20%  "v2";   # 20% traffic ke versi baru
        *    "v1";   # 80% traffic ke versi lama
    }

    upstream app_v1 { server localhost:3000; }
    upstream app_v2 { server localhost:3001; }

    server {
        location / {
            proxy_pass http://app_$app_version;
            # 20% request → http://app_v2 (localhost:3001)
            # 80% request → http://app_v1 (localhost:3000)

            add_header X-App-Version $app_version always;
        }
    }
}
# Routing berdasarkan custom header
map $http_x_api_version $upstream_name {
    "v1"    api_v1_backend;
    "v2"    api_v2_backend;
    default api_v2_backend;  # Default ke versi terbaru
}

upstream api_v1_backend { server localhost:3000; }
upstream api_v2_backend { server localhost:3001; }

server {
    location /api/ {
        proxy_pass http://$upstream_name;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        # X-API-Version: v1 → diteruskan ke localhost:3000
        # X-API-Version: v2 → diteruskan ke localhost:3001
    }
}

Ringkasan #

  • Tanpa URI di proxy_pass: URI diteruskan apa adanya. Dengan URI (termasuk /): bagian location digantikan — ini perbedaan kritis yang sering menyebabkan bug.
  • Selalu gunakan proxy_http_version 1.1 dan proxy_set_header Connection "" untuk keepalive dan fitur HTTP modern.
  • Unix socket (http://unix:/path/to/sock) 10-30% lebih cepat dari TCP localhost untuk komunikasi antar proses di mesin yang sama.
  • WebSocket membutuhkan Upgrade dan Connection header, plus timeout yang panjang (proxy_read_timeout 3600s).
  • Gunakan map $http_upgrade $connection_upgrade untuk menentukan nilai Connection header secara kondisional — diperlukan untuk server yang melayani HTTP reguler dan WebSocket sekaligus.
  • Selalu sertakan X-Real-IP, X-Forwarded-For, dan X-Forwarded-Proto agar backend tahu informasi asli klien.

← Sebelumnya: Konsep Reverse Proxy   Berikutnya: Proxy Headers →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact