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_connectionkullanmayı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 -tulpnile backend’in dinleyip dinlemediğini kontrol edin. - 400 Bad Request:
Upgradebaşlığı iletilmiyor.proxy_http_version 1.1satırının olduğundan emin olun. - Bağlantı aniden kesiliyor: Timeout değerleri düşük.
proxy_read_timeoutdeğerini artırın ve uygulamanızda heartbeat mekanizması ekleyin. - 101 alınıyor ama mesaj gelmiyor:
proxy_buffering offsatı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 offolmadan 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.
