Nginx ile WebSocket Proxy Yapılandırması

Gerçek zamanlı uygulamalar geliştirirken ya da böyle bir sistemi production’a taşırken en sık karşılaşılan zorluklardan biri WebSocket bağlantılarını doğru şekilde proxy etmektir. HTTP’nin stateless yapısından farklı olarak WebSocket, sürekli açık kalan bir TCP bağlantısı üzerinden çalışır ve bu durum Nginx yapılandırmasında birkaç kritik detayı zorunlu kılar. Yanlış yapılandırılmış bir proxy, bağlantının saniyeler içinde düşmesine, 101 Switching Protocols hatasına ya da tamamen sessiz başarısızlıklara neden olabilir.

WebSocket ve HTTP Farkı: Neden Özel Yapılandırma Gerekir?

WebSocket bağlantısı, aslında bir HTTP isteğiyle başlar. İstemci, sunucuya Upgrade: websocket başlığı içeren bir HTTP/1.1 isteği gönderir. Sunucu 101 durum kodu ile yanıt verirse protokol WebSocket’e yükseltilir ve bağlantı açık kalır. İşte burada Nginx devreye giriyor.

Nginx varsayılan olarak Upgrade ve Connection başlıklarını upstream’e iletmez. Bu başlıklar proxy katmanında kaybolur ve backend’iniz WebSocket handshake’ini hiç göremez. Bağlantı ya tamamen reddedilir ya da HTTP olarak devam etmeye çalışır ve patlar.

Bunun yanı sıra Nginx’in varsayılan proxy timeout değerleri genellikle 60 saniyedir. Bir WebSocket bağlantısı aktif veri alışverişi olmadan 60 saniye beklerse Nginx tarafında kesilir. Chat uygulaması, canlı borsa takibi, oyun sunucusu gibi durumlarda bu değerlerin çok daha yüksek tutulması gerekir.

Temel WebSocket Proxy Yapılandırması

En sade haliyle çalışan bir yapılandırmadan başlayalım. Diyelim ki Node.js tabanlı bir WebSocket sunucunuz localhost:3000 üzerinde çalışıyor ve bunu wss://app.example.com üzerinden dışarıya açmak istiyorsunuz.

# /etc/nginx/sites-available/websocket-app
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;

    location /ws/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        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;
    }
}

Buradaki kritik satırlar şunlar:

  • proxy_http_version 1.1: WebSocket yalnızca HTTP/1.1 üzerinde çalışır. Nginx varsayılan olarak HTTP/1.0 kullanır, bunu açıkça belirtmezseniz Upgrade başlığı desteklenmez.
  • proxy_set_header Upgrade $http_upgrade: İstemciden gelen Upgrade başlığını backend’e iletir.
  • proxy_set_header Connection “upgrade”: Connection başlığını sabit “upgrade” değeriyle gönderir. Burada $http_connection kullanmayın, bazı istemciler farklı değerler gönderebilir.

Timeout Değerlerinin Doğru Ayarlanması

Production ortamında en çok sorun çıkaran konu timeout’lardır. Bir WebSocket bağlantısı uzun süre susarsa (heartbeat yoksa ya da düşükse) Nginx bu bağlantıyı ölü sayıp keser.

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;

    location /ws/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # Timeout ayarları
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_connect_timeout 10s;

        # Buffer'ları WebSocket için kapat
        proxy_buffering off;
    }
}
  • proxy_read_timeout: Backend’den yanıt beklenecek maksimum süre. WebSocket için 1 saat (3600s) makul bir başlangıç değeridir.
  • proxy_send_timeout: Nginx’in istemciye veri gönderebileceği süre.
  • proxy_connect_timeout: Backend’e ilk bağlantı için timeout. Bunu çok uzun tutmayın.
  • proxy_buffering off: WebSocket mesajlarının tamponlanmadan anında iletilmesi için kapatılması gerekir. Aksi halde mesajlar birikir ve gecikmeli gelir.

Load Balancer ile WebSocket Sticky Session

Birden fazla Node.js instance’ınız varsa ve bunları upstream olarak tanımladıysanız, WebSocket’in sticky session (yapışkan oturum) gerektirdiğini unutmamalısınız. WebSocket bağlantısı kurulduktan sonra tüm paketlerin aynı backend’e gitmesi gerekir.

# /etc/nginx/conf.d/upstream.conf
upstream websocket_backend {
    # ip_hash ile aynı IP her zaman aynı sunucuya gider
    ip_hash;

    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;

    keepalive 32;
}

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;

    location /ws/ {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        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_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }

    location / {
        proxy_pass http://websocket_backend;
        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;
    }
}

ip_hash yerine hash $cookie_sessionid consistent; kullanmak daha akıllıca bir yaklaşım olabilir. Kullanıcının IP’si değil, oturum cookie’si üzerinden yönlendirme yapılır ve NAT arkasındaki kullanıcılarda daha dengeli dağılım sağlanır.

upstream websocket_backend {
    hash $cookie_sessionid consistent;

    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;

    keepalive 32;
}

HTTP ve WebSocket Trafiğini Aynı Port’ta Ayırt Etmek

Gerçek dünyada çoğu uygulama hem REST API hem de WebSocket bağlantısı sunar. Bunları aynı domain ve port üzerinden yönetmek için location bloklarını düzgün yapılandırmanız gerekir.

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    # HTTP/2 için
    http2 on;

    # WebSocket endpoint'i
    location /socket.io/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        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;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }

    # REST API endpoint'leri
    location /api/ {
        proxy_pass http://localhost: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;

        # API için rate limiting
        limit_req zone=api_limit burst=20 nodelay;
    }

    # Statik dosyalar
    location / {
        root /var/www/app/public;
        try_files $uri $uri/ /index.html;
        expires 1d;
        add_header Cache-Control "public, no-transform";
    }
}

# Rate limiting zone tanımı (http bloğunda olmalı)
# /etc/nginx/nginx.conf içindeki http {} bloğuna ekleyin
# limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/s;

Upgrade Header’ının Dinamik Yönetimi

Bazı durumlarda aynı location bloğunda hem normal HTTP hem de WebSocket isteklerini kabul etmek istersiniz. $http_upgrade değişkeninin boş olduğu durumlarda Connection başlığını close olarak göndermek gerekebilir. Bunun için bir map kullanabilirsiniz.

# /etc/nginx/nginx.conf içindeki http {} bloğuna ekleyin
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

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;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $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;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
}

Bu yapılandırma oldukça elegandır. İstek WebSocket ise Connection: upgrade gönderilir, normal HTTP ise Connection: close gönderilir.

SSL/TLS Terminasyonu ve Güvenlik Sertleştirme

WSS (WebSocket Secure) bağlantıları için Nginx’te SSL terminasyonu yapıldığında backend’e ws:// üzerinden bağlanılabilir. Bu iç ağda şifreleme yükünü azaltır ama güvenlik politikanıza göre değişebilir.

# Güvenlik sertleştirmeli tam yapılandırma
server {
    listen 443 ssl;
    server_name app.example.com;

    # SSL Sertifika
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    # Modern SSL ayarları
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # WebSocket için güvenlik başlıkları
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    # Origin kontrolü - sadece kendi domain'inizden gelen bağlantılara izin verin
    location /ws/ {
        # Origin başlığını kontrol et
        if ($http_origin !~* "^https://app.example.com$") {
            return 403;
        }

        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        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;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
}

Origin kontrolü hakkında not: Production’da if bloğu kullanmak Nginx’te önerilmez ancak basit origin kontrolü için tolere edilebilir. Daha karmaşık senaryolarda bu kontrolü uygulama seviyesinde yapın.

Yapılandırmayı Test Etme ve Hata Ayıklama

Yapılandırmayı uygulamadan önce ve sonra mutlaka test etmelisiniz.

# Nginx yapılandırmasını kontrol et
sudo nginx -t

# Başarılı çıktı şöyle görünmeli:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# Reload (tam yeniden başlatma değil, aktif bağlantıları kesmez)
sudo systemctl reload nginx

# WebSocket bağlantısını test etmek için wscat aracını kullanın
npm install -g wscat

# WSS bağlantısı test
wscat -c wss://app.example.com/ws/

# Nginx error log'larını canlı izle
sudo tail -f /var/log/nginx/error.log

# WebSocket ile ilgili logları filtrele
sudo grep -i "upgrade|websocket|101" /var/log/nginx/access.log

# Bağlantı durumunu kontrol et
sudo ss -tulpn | grep nginx
sudo netstat -an | grep :443 | grep ESTABLISHED | wc -l

Hata ayıklarken özellikle şu durumları kontrol edin:

  • 502 Bad Gateway: Backend çalışmıyor ya da yanlış port. ss -tulpn ile backend’in dinleyip dinlemediğini kontrol edin.
  • 400 Bad Request: Upgrade başlığı iletilmiyor. proxy_http_version 1.1 satırının olduğundan emin olun.
  • Bağlantı aniden kesiliyor: Timeout değerleri düşük. proxy_read_timeout değerini artırın ve uygulamanızda heartbeat mekanizması ekleyin.
  • 101 alınıyor ama mesaj gelmiyor: proxy_buffering off satırını ekleyin.

Gerçek Dünya Senaryosu: Socket.IO ile Chat Uygulaması

Socket.IO kullanan bir chat uygulamasının Nginx yapılandırması biraz farklıdır çünkü Socket.IO önce HTTP long-polling ile bağlanır, sonra WebSocket’e yükseltmeye çalışır.

# /etc/nginx/sites-available/chat-app
upstream socketio_backend {
    ip_hash;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    keepalive 64;
}

server {
    listen 80;
    server_name chat.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name chat.example.com;

    ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Socket.IO - hem polling hem websocket için
    location /socket.io/ {
        proxy_pass http://socketio_backend;
        proxy_http_version 1.1;
        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;

        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;

        # Long polling için gerekli
        proxy_cache_bypass 1;
        proxy_no_cache 1;
    }

    # API endpoint'leri
    location /api/ {
        proxy_pass http://socketio_backend;
        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;
    }

    # React/Vue frontend
    location / {
        root /var/www/chat-app/build;
        try_files $uri $uri/ /index.html;
        expires 1h;
    }
}

Monitoring ve Log Formatı

WebSocket bağlantılarını izlemek için Nginx log formatını zenginleştirmek işe yarar.

# /etc/nginx/nginx.conf içindeki http {} bloğuna ekleyin
log_format websocket_log '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $body_bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         'upgrade="$http_upgrade" '
                         'rt=$request_time '
                         'uct=$upstream_connect_time '
                         'urt=$upstream_response_time';

server {
    listen 443 ssl;
    server_name app.example.com;

    access_log /var/log/nginx/websocket_access.log websocket_log;
    error_log /var/log/nginx/websocket_error.log warn;

    location /ws/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        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_read_timeout 3600s;
        proxy_buffering off;
    }
}

Prometheus + Nginx Exporter veya Datadog gibi araçlar kullanıyorsanız aktif WebSocket bağlantı sayısını nginx_connections_active metrikiyle takip edebilirsiniz. Anormal bir artış ya da ani düşüş hemen alert üretmelidir.

Yaygın Hatalar ve Çözümleri

Nginx WebSocket yapılandırmasında en sık karşılaşılan hataları şöyle özetleyebiliriz:

  • “Connection” başlığında $http_connection kullanmak: İstemciden gelen Connection değeri her zaman “upgrade” olmayabilir. Sabit “upgrade” yazın ya da map kullanın.
  • proxy_http_version 1.1 eksikliği: HTTP/1.0 Upgrade başlığını desteklemez. Bu satır olmadan hiçbir şey çalışmaz.
  • Buffering’i kapatmamak: proxy_buffering off olmadan mesajlar gecikir veya hiç gelmez.
  • Timeout değerlerini düşük bırakmak: 60 saniyelik varsayılan değer çoğu WebSocket uygulaması için yetersizdir.
  • Load balancer’da sticky session unutmak: Birden fazla backend varsa ip_hash veya cookie hash kullanın.
  • SSL’i backend’e kadar uzatmamak ama da termination’ı unutmak: Backend’e X-Forwarded-Proto başlığını göndermeyi unutursanız uygulama kendi adresini http:// olarak görür.

Sonuç

Nginx ile WebSocket proxy yapılandırması aslında birkaç kritik satırdan ibaret, ancak bu satırları kaçırdığınızda sorunları debug etmek epey can sıkıcı olabiliyor. Özet olarak şunları aklınızda tutun:

proxy_http_version 1.1 ve Upgrade/Connection başlıklarını iletmek olmadan WebSocket handshake imkansız. proxy_buffering off olmadan mesajlar gecikir. Timeout değerlerini uygulamanızın heartbeat mantığına göre ayarlayın. Load balancer kullanıyorsanız sticky session şart.

Production’a geçmeden önce mutlaka wscat ile el ile test edin, timeout senaryolarını simüle edin ve log formatınızı WebSocket bilgilerini kapsayacak şekilde genişletin. Çoğu sorun yapılandırma hatası değil, eksik başlık ya da yanlış timeout değerinden kaynaklanıyor. Bir kez doğru kurduğunuzda Nginx, WebSocket trafiğini son derece verimli şekilde yönetiyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir