ERETEK
BİLGİ SİSTEMLERİ

Nginx Reverse Proxy Yapılandırması: Başlıklar, Timeout, WebSocket

Nginx reverse proxy’de yanlış istemci IP’si, HTTP’ye düşen yönlendirme, kopan WebSocket ve 502/504: başlıklar, timeout, buffer, keepalive ve teşhis sırası.

Bu makalede8 bölüm
Teknik özet
Teknoloji
Nginx, ngx_http_proxy_module, ngx_http_realip_module, WebSocket, PHP-FPM, Docker
Konu
Uygulamaları Nginx arkasında doğru yayınlamak
Seviye
Orta–ileri
Ön koşullar
  • Linux sunucuda Nginx kurulu ve temel server bloğu bilgisi
  • Arka uç uygulamanın dinlediği adres ve port
  • Yapılandırmayı test edip yeniden yükleme yetkisi

Uygulamayı Nginx arkasına aldınız, site açılıyor. Birkaç gün sonra loglarda bütün ziyaretçiler 127.0.0.1’den geliyor, ödeme dönüşü HTTP’ye yönleniyor, WebSocket bağlantısı tam 60. saniyede kopuyor, yoğun saatte 502 düşüyor. Bunların neredeyse hepsi varsayılan değerlerle bırakılmış bir reverse proxy’nin sonucudur. Node.js, Python, Java ya da container içindeki bir uygulamanın önüne Nginx koymak TLS sonlandırma, sıkıştırma, statik dosya ve erişim kontrolünü tek noktada toplar; ama o noktadaki her direktif bilinçli yazılmalı. Aşağıda üretimde kullanılan iskeleti direktif direktif açıyor, 502/504 teşhisinde konsolda izlenecek sırayı veriyoruz.

Reverse proxy’nin görevleri

İstemci yalnızca reverse proxy ile konuşur; arkadaki uygulama sunucularını görmez. Kurumsal web altyapısında Nginx bu rolde şu işleri üstlenir:

  • TLS sonlandırma: Sertifika ve protokol ayarları uygulamadan bağımsız yönetilir.
  • Bağlantı tamponlama: Yavaş istemcilerin uygulama işçilerini meşgul etmesini engeller; yanıt Nginx’e hızla aktarılır, istemciye Nginx gönderir.
  • Yönlendirme: Aynı alan adında /api/ bir servise, / başka bir servise gidebilir.
  • Yük dağıtımı ve sağlık: Birden fazla arka uç arasında dağıtım ve hatalı sunucuyu geçici olarak devreden çıkarma.
  • Politika noktası: İstek boyutu sınırı, rate limit, IP kısıtlaması, güvenlik başlıkları.

Bu işlerin her biri ayrı bir arıza noktasıdır. Web sunucusu seçimi ise ayrı bir karar; onu Nginx, Apache ve IIS karşılaştırması yazısında tartıştık. Buradan sonrası Nginx üzerine.

Temel proxy yapılandırması

Aşağıdaki iskelet 127.0.0.1:3000’de dinleyen bir uygulamayı yayınlar. Arka ucu baştan upstream bloğunda tanımlayın; ikinci sunucuyu eklemek ve keepalive açmak sonradan tek satırlık iş olur.

upstream app_backend {
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;
    # ssl_certificate ve ssl_certificate_key satırları burada

    client_max_body_size 20m;

    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        include /etc/nginx/snippets/proxy-headers.conf;
    }
}

http2 on; direktifi Nginx 1.25.1 ile geldi; daha eski sürümlerde listen 443 ssl http2; sözdizimi kullanılır. client_max_body_size varsayılan olarak 1 MB’tır. Dosya yükleyen bir uygulamayı Nginx arkasına taşıdıktan sonra gelen ilk şikâyet genelde budur: 413 Request Entity Too Large. Değeri uygulamanın kendi yükleme sınırıyla eşitleyin.

proxy_pass ve eğik çizgi kuralı

Taşıma sonrası açıklanamayan 404’lerin baş şüphelisi proxy_pass değerinin sonundaki URI parçasıdır:

Yapılandırma İstek Arka uca giden yol
location /api/ { proxy_pass http://app; } /api/users /api/users
location /api/ { proxy_pass http://app/; } /api/users /users
location /api/ { proxy_pass http://app/v2/; } /api/users /v2/users

Kural şudur: proxy_pass içinde bir URI (en az tek bir /) varsa, location ile eşleşen önek o URI ile değiştirilir. Regex ile tanımlanmış location’larda URI içeren proxy_pass kullanılamaz (nginx -t hata verir); rewrite … break ile URI değiştirildiğinde ise proxy_pass içindeki URI yok sayılır ve değiştirilmiş yol iletilir. Bu durumlarda URI’siz proxy_pass kullanıp yolu açıkça yeniden yazmak daha öngörülebilirdir. Tek karakter, bambaşka davranış.

İstemci IP’si ve protokol başlıkları

Uygulama TCP bağlantısını Nginx’ten alır. İstemcinin IP’sini, kullandığı protokolü ve istediği alan adını yalnızca başlıklardan öğrenebilir. Bu başlıkları her server bloğuna ayrı ayrı yazmak yerine tek bir snippet dosyasında toplayın:

# /etc/nginx/snippets/proxy-headers.conf
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;

Nginx varsayılan olarak arka uca Host başlığında proxy_pass içindeki adı ($proxy_host) gönderir; çok kiracılı uygulamalar ve mutlak URL üreten çatılar bu yüzden kullanıcıyı app_backend gibi bir adrese yönlendirir. X-Forwarded-Proto eksik olduğunda uygulama isteğin HTTP olduğunu sanır; oturum çerezini Secure işaretlemez ya da sonsuz HTTPS yönlendirme döngüsüne girer.

Asıl tuzak kalıtım kuralındadır: bir location içinde tek bir proxy_set_header tanımlarsanız, üst bloktaki tüm proxy_set_header tanımları o location için geçersiz olur. nginx -t bunu yakalamaz, çünkü yapılandırma geçerlidir. Snippet’i her location’a include etmek tuzağı ortadan kaldırır. Uygulama tarafında da proxy’ye güvenmeyi açıkça tanımlayın (Express’te trust proxy, Django’da SECURE_PROXY_SSL_HEADER, Laravel’de TrustProxies ara katmanı); aksi halde başlıklar yok sayılır.

Cloudflare veya başka bir proxy arkasında gerçek IP

Nginx’in önünde Cloudflare, bir yük dengeleyici ya da WAF varsa $remote_addr o cihazın adresidir. ngx_http_realip_module ile yalnızca güvendiğiniz kaynaklardan gelen başlığa itibar edilir:

# http bloğunda; aralıkların tamamını https://www.cloudflare.com/ips/ listesinden alın
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 2400:cb00::/32;
# ... listedeki diğer IPv4 ve IPv6 aralıkları
real_ip_header CF-Connecting-IP;

Önde kendi yük dengeleyiciniz varsa real_ip_header X-Forwarded-For; ve real_ip_recursive on; kullanılır; bu durumda güvenilen tüm ara proxy’lerin adresleri set_real_ip_from ile tanımlanmalıdır. set_real_ip_from 0.0.0.0/0 yazmak, herkesin başlık göndererek IP sahteciliği yapmasına izin verir; “çalışsın diye” yazılmış bu satırla sahada sık karşılaşılır ve o andan itibaren rate limit ile IP kısıtlaması anlamını yitirir. Cloudflare’in IP listesi değişebilir. Bu dosyayı periyodik güncelleyen bir görev kurun. Cloudflare ile önbellek katmanının birlikte nasıl çalıştığı için Cloudflare, WP Rocket ve Redis rehberine bakabilirsiniz.

Zaman aşımı ve buffer ayarları

Nginx’in üç proxy zaman aşımı vardır ve hiçbiri toplam istek süresini ölçmez; her biri iki işlem arasındaki boşluğa uygulanır:

Direktif Varsayılan Ne ölçer
proxy_connect_timeout 60s Arka uçla TCP bağlantısı kurma süresi (genellikle 75 saniyeyi aşamaz)
proxy_send_timeout 60s Arka uca iki yazma işlemi arası
proxy_read_timeout 60s Arka uçtan iki okuma işlemi arası

Kural basit: bağlantı zaman aşımını kısa tutun (aynı sunucudaki bir servis için birkaç saniye yeterlidir), okuma zaman aşımını ise yalnızca uzun süren uçlar için location bazında uzatın. Rapor üreten bir /export/ yolu için 300 saniye tanımlamak, tüm siteye 300 saniye vermekten daha güvenlidir; aksi halde takılan istekler işçi bağlantılarını uzun süre tutar. Kalıcı çözüm mimaridedir: uzun işi kuyruğa alın, istemci durumu sorgulasın. Zaman aşımını büyütmek tedavi değildir.

Buffer tarafında varsayılan proxy_buffering on çoğu senaryoda doğrudur. Elle dokunmanız gereken üç nokta var:

  • proxy_buffer_size: Yanıtın başlık kısmı için kullanılır (varsayılan bir bellek sayfası, 4k veya 8k). Büyük çerez veya JWT taşıyan uygulamalarda loglarda upstream sent too big header görülür ve istemci 502 alır; 16k–32k’ya çıkarmak çoğunlukla yeterlidir.
  • proxy_buffers ve proxy_busy_buffers_size: Gövde tamponları. Bellek yetmezse yanıt geçici dosyaya yazılır; logdaki an upstream response is buffered to a temporary file uyarısı bunu gösterir.
  • Server-Sent Events veya akış yapan uçlarda o location için proxy_buffering off; tanımlayın ya da uygulamanın X-Accel-Buffering: no başlığı göndermesini sağlayın.

WebSocket ve upstream keepalive

WebSocket, HTTP/1.1 Upgrade mekanizmasıyla başlar. Upgrade ve Connection hop-by-hop başlıklardır; Nginx bunları arka uca kendiliğinden iletmez. Siz aktarmazsanız el sıkışma hiç tamamlanmaz. nginx.org’un WebSocket proxying belgesinde önerilen yapı:

# http bloğunda
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# server bloğunda
location /ws/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    include /etc/nginx/snippets/proxy-headers.conf;
    proxy_read_timeout 1h;
}

Bu location’da Connection "" yerine $connection_upgrade var; ikisini karıştırmak WebSocket’i sessizce bozar. Normal HTTP istekleri için ise upstream keepalive devreye girer: keepalive 32; her işçi sürecin arka uca açık tutacağı boşta bağlantı sayısının üst sınırıdır. Çalışması için proxy_http_version 1.1 ve boş Connection başlığı gerekir; Nginx varsayılan olarak arka uçla HTTP/1.0 konuşur ve her istekte yeni bağlantı açar. Yoğun trafikte keepalive olmadan sunucuda çok sayıda TIME_WAIT soketi birikir ve geçici port tükenmesi yaşanabilir. ss -s çıktısında timewait sayısı binlerle ölçülüyorsa ilk bakılacak yer burasıdır.

Docker ile container tabanlı yayında arka uç bir servis adıyla tanımlanıyorsa, Nginx bu adı yalnızca yapılandırma yüklenirken çözer; container yeniden oluşturulup IP’si değişirse Nginx eski adrese gitmeye devam eder. “Container yeniden başladı, site 502 veriyor” şikâyetinin klasik nedeni budur. Ya arka uç değiştiğinde Nginx’i yeniden yükleyin ya da resolver 127.0.0.11 valid=10s; ile değişken tabanlı proxy_pass kullanın. Container yığınının üretim ayarları Docker Compose üretim yayını rehberinde ayrıntılı anlatılıyor.

502 ve 504 hatalarının teşhisi

İki hata kodu farklı şeyler söyler: 502 Bad Gateway, Nginx’in arka uçtan geçerli bir yanıt alamadığını; 504 Gateway Timeout ise yanıtın zaman aşımı içinde gelmediğini gösterir. Tarayıcıdaki hata sayfası size hiçbir şey söylemez. Teşhis error.log ile başlar.

error.log mesajı Kod Olası neden ve kontrol
connect() failed (111: Connection refused) 502 Uygulama çalışmıyor veya farklı port/adreste dinliyor; ss -ltnp ile doğrulayın
connect() to unix:… failed (13: Permission denied) 502 Soket dosyası izinleri veya SELinux; PHP-FPM havuzunda listen.owner/listen.group
connect() failed (13: Permission denied), TCP 502 RHEL ailesinde SELinux; setsebool -P httpd_can_network_connect 1
upstream prematurely closed connection 502 Uygulama işlem ortasında çöktü veya bellek limitine takıldı; uygulama logları
upstream sent too big header 502 proxy_buffer_size veya fastcgi_buffer_size yetersiz
no live upstreams 502 Tüm sunucular max_fails nedeniyle geçici olarak devre dışı
upstream timed out (110: Connection timed out) while reading response header 504 Yavaş sorgu veya kilitlenme; proxy_read_timeout artırmak yalnızca belirtiyi gizler

Konsolda izlenecek sıra:

  1. nginx -t ile yapılandırmanın geçerli olduğunu doğrulayın; nginx -T birleşik yapılandırmayı dökerek beklenen dosyanın gerçekten yüklendiğini gösterir.
  2. Arka ucu Nginx’i atlayarak sunucunun kendisinden deneyin: curl -sv -H "Host: app.example.com" http://127.0.0.1:3000/. Burada da hata varsa sorun proxy’de değildir.
  3. Erişim loguna $upstream_addr, $upstream_status, $upstream_response_time ve $request_time ekleyin. İki süre arasındaki fark, zamanın Nginx–istemci tarafında mı yoksa uygulamada mı harcandığını gösterir.
  4. 499 kodlarına bakın: istemci yanıtı beklemeden bağlantıyı kapatmıştır; genellikle önünüzdeki CDN veya yük dengeleyicinin zaman aşımı Nginx’inkinden kısadır.
  5. Zaman aşımı zincirini hizalayın: CDN > Nginx > uygulama sunucusu > veritabanı sorgu sınırı şeklinde dıştan içe azalan değerler, hatanın kaynağa en yakın katmanda ve anlamlı bir mesajla oluşmasını sağlar.

proxy_next_upstream varsayılan olarak error timeout durumlarında isteği bir sonraki sunucuya dener; Nginx 1.9.13’ten beri POST gibi idempotent olmayan istekler, arka uca gönderildikten sonra yeniden denenmez. Bu davranışı non_idempotent ile genişletmek, ödeme veya sipariş oluşturan uçlarda çift kayıt riskini getirir. Varsayılanı bırakın.

PHP-FPM kullanan sitelerde aynı teşhis mantığı fastcgi_pass ve fastcgi_read_timeout gibi karşılıklar üzerinden yürür; havuz tükenmesi kaynaklı 502/504’ler için PHP-FPM ve MariaDB performans rehberi doğrudan uygulanabilir. Tüm direktiflerin resmi tanımları ngx_http_proxy_module belgesindedir.

Yayına almadan önce kontrol listesi

  • Host, X-Forwarded-For, X-Forwarded-Proto her location’da iletiliyor; uygulama proxy’ye güvenecek şekilde ayarlı.
  • Önde CDN veya yük dengeleyici varsa realip yalnızca onların aralıklarına güveniyor.
  • client_max_body_size uygulamanın yükleme sınırıyla uyumlu.
  • Zaman aşımları location bazında ve dış katmandan içe doğru azalan sırada.
  • WebSocket yolları Upgrade/Connection başlıklarını aktarıyor ve okuma zaman aşımı uygulamanın ping aralığından uzun.
  • Upstream keepalive ile proxy_http_version 1.1 birlikte tanımlı.
  • Erişim logunda upstream süreleri ve durum kodları kayıtlı.

TLS protokol ve şifre ayarları ayrı bir konu; onlar için web sunucusunda TLS yapılandırması rehberine geçin. Çok sayıda uygulamayı aynı Nginx arkasında toplayan yapılarda asıl iş yapılandırma standardı ve izlemedir; bu kurguyu Nginx web sunucusu ve reverse proxy hizmetimiz kapsamında kuruyoruz.

Sık sorulan sorular

proxy_pass adresinin sonundaki eğik çizgi neyi değiştirir?

proxy_pass içinde URI parçası varsa (örneğin http://app/ ile biten adres), location ile eşleşen önek bu URI ile değiştirilir; /api/users isteği arka uca /users olarak gider. URI yoksa (http://app) yol olduğu gibi iletilir. Taşıma sonrasında çıkan beklenmedik 404’lerin sık nedeni bu farktır.

WebSocket bağlantılarım tam 60 saniyede kopuyor, neden?

proxy_read_timeout varsayılan olarak 60 saniyedir ve iki okuma arasındaki boşluğa uygulanır. Uygulama bu sürede hiç veri göndermezse Nginx bağlantıyı kapatır. İlgili location için zaman aşımını uzatın ya da uygulamada periyodik ping/pong kullanın; ikincisi aradaki diğer proxy ve yük dengeleyicilerde de işe yarar.

Cloudflare arkasında loglarda neden hep Cloudflare IP’leri görünüyor?

Nginx bağlantıyı Cloudflare’in uç sunucusundan aldığı için $remote_addr o adresi gösterir. realip modülüyle set_real_ip_from satırlarına Cloudflare’in yayımladığı IP aralıklarını ekleyip real_ip_header CF-Connecting-IP tanımladığınızda loglar, rate limit ve erişim kuralları gerçek istemci IP’siyle çalışır.

PHP-FPM için de proxy_pass mi kullanılmalı?

Hayır. PHP-FPM FastCGI konuşur; Nginx’te fastcgi_pass ile bağlanılır ve zaman aşımı, buffer gibi ayarların fastcgi_ önekli karşılıkları kullanılır. Yukarıdaki zaman aşımı, buffer ve teşhis mantığı aynen geçerlidir; istemci bilgisi ise proxy_set_header yerine fastcgi_param ile aktarılır.

Teknik özet
Teknoloji
Nginx, ngx_http_proxy_module, ngx_http_realip_module, WebSocket, PHP-FPM, Docker
Konu
Uygulamaları Nginx arkasında doğru yayınlamak
Seviye
Orta–ileri
Ön koşullar
  • Linux sunucuda Nginx kurulu ve temel server bloğu bilgisi
  • Arka uç uygulamanın dinlediği adres ve port
  • Yapılandırmayı test edip yeniden yükleme yetkisi
Eretek uzman desteği

Bu konu sizin ortamınızda nasıl çalışır, birlikte bakalım.

Mevcut yapınızı inceliyor, darboğazı tespit ediyor ve uygulanabilir bir iş planı öneriyoruz.