Caddy ile WebSocket Proxy Yapılandırması

Modern web uygulamalarında WebSocket artık bir lüks değil, neredeyse zorunluluk haline geldi. Chat uygulamaları, gerçek zamanlı dashboard’lar, online oyunlar, canlı bildirim sistemleri… Bunların hepsi WebSocket’e ihtiyaç duyuyor. Nginx veya Apache ile WebSocket proxy yapılandırması yaparken elle upgrade header’ları yazmak, timeout ayarlarını tek tek düzenlemek zorunda kalıyorduk. Caddy ise bu işi büyük ölçüde otomatikleştiriyor. Bu yazıda Caddy ile WebSocket proxy yapılandırmasını gerçek dünya senaryolarıyla birlikte detaylıca ele alacağız.

WebSocket ve Reverse Proxy Mantığı

Önce temeli oturtmak gerekiyor. WebSocket, HTTP üzerinden başlayan ama sonra kalıcı bir TCP bağlantısına yükseltilen (upgrade) bir protokol. Tarayıcı sunucuya önce HTTP isteği atıyor, Upgrade: websocket header’ı ile “hadi bunu WebSocket’e çevirelim” diyor. Sunucu kabul ederse bağlantı sürekli açık kalıyor ve iki taraf birbirine veri itebiliyor.

Bir reverse proxy bu senaryoda ortada durunca işler biraz karmaşıklaşıyor. Proxy’nin:

  • Upgrade ve Connection header’larını doğru iletmesi
  • Bağlantının uzun süre açık kalmasına izin vermesi
  • Timeout değerlerini akıllıca yönetmesi
  • TLS termination yapıyorsa wss:// ile ws:// arasındaki dönüşümü halletmesi

gerekiyor. Nginx’te bunu manuel yapmak zorundaydık. Caddy ise reverse_proxy direktifi ile bunların büyük bölümünü otomatik hallediyor.

Temel Caddy WebSocket Proxy Yapılandırması

En basit senaryodan başlayalım. Diyelim ki localhost:3000 adresinde çalışan bir Node.js uygulamanız var ve bu uygulama hem HTTP hem de WebSocket istekleri alıyor.

# /etc/caddy/Caddyfile

myapp.example.com {
    reverse_proxy localhost:3000
}

Evet, bu kadar. Caddy, gelen isteğin WebSocket upgrade isteği olduğunu otomatik algılıyor ve gerekli header manipülasyonlarını yapıyor. Arka planda Connection: Upgrade ve Upgrade: websocket header’larını backend’e iletiyor.

Peki ya farklı path’lerde farklı backend’leriniz varsa? Örneğin /api normal HTTP, /ws ise WebSocket endpoint’i olsun:

myapp.example.com {
    handle /ws* {
        reverse_proxy localhost:3001
    }

    handle /api* {
        reverse_proxy localhost:3000
    }

    handle {
        root * /var/www/myapp
        file_server
    }
}

Bu yapıda /ws ile başlayan tüm istekler 3001 portundaki WebSocket sunucusuna, /api istekleri 3000’e, geri kalanlar ise statik dosya sunucusuna gidiyor.

Header Yönetimi ve Güvenlik

Caddy, varsayılan olarak bazı header’ları backend’e iletirken bazılarını düzenliyor. WebSocket bağlantılarında özellikle dikkat etmemiz gereken birkaç header var.

myapp.example.com {
    reverse_proxy localhost:3000 {
        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

header_up Host: Backend sunucusunun doğru Host değerini görmesini sağlıyor. Özellikle sanal host kullanan uygulamalarda kritik.

header_up X-Real-IP: Gerçek istemci IP’sini backend’e iletmek için. Chat uygulamalarında rate limiting, güvenlik logları için şart.

header_up X-Forwarded-Proto: İstemcinin HTTPS üzerinden mi yoksa HTTP üzerinden mi bağlandığını backend’e bildiriyor. WebSocket açısından bu wss:// mi ws:// mi sorusunun cevabı demek.

Node.js tarafında bu header’lara erişmek için:

# Node.js uygulamanızda Express kullanıyorsanız
# trust proxy ayarını yapmayı unutmayın
# app.set('trust proxy', true)

# Bağlantı logunu test etmek için
curl -v 
  -H "Connection: Upgrade" 
  -H "Upgrade: websocket" 
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" 
  -H "Sec-WebSocket-Version: 13" 
  https://myapp.example.com/ws

Timeout ve Bağlantı Yönetimi

WebSocket bağlantılarının en kritik özelliği uzun süre açık kalmalarıdır. Bir chat uygulamasında kullanıcı saatlerce bağlı kalabilir. Caddy’nin varsayılan timeout değerleri bu senaryolar için biraz kısa gelebilir.

myapp.example.com {
    reverse_proxy localhost:3000 {
        # Backend'e bağlantı timeout'u
        transport http {
            dial_timeout 10s
            response_header_timeout 30s
            # WebSocket için keep-alive
            keepalive 30s
            keepalive_idle_conns 100
        }

        # Sağlık kontrolü
        health_uri /health
        health_interval 30s
        health_timeout 10s
    }
}

dial_timeout: Caddy’nin backend’e bağlanmak için beklediği süre. 10 saniye makul bir değer.

response_header_timeout: Backend’in header döndürmesi için beklenen süre. WebSocket handshake için 30 saniye genellikle yeterli.

keepalive: HTTP/1.1 keep-alive süresi. WebSocket bağlantıları bundan bağımsız ama HTTP istekler için önemli.

Bir gerçek dünya senaryosu düşünelim: Canlı kripto fiyatları gösteren bir dashboard. Kullanıcılar ortalama 15-20 dakika sayfada kalıyor ve sürekli fiyat güncellemesi alıyor. Bu senaryo için:

crypto-dashboard.example.com {
    encode gzip

    reverse_proxy /ws/* localhost:8080 {
        transport http {
            dial_timeout 5s
            response_header_timeout 10s
        }

        # Aktif bağlantı sayısını sınırla
        max_conns_per_host 1000
    }

    reverse_proxy /api/* localhost:8081

    handle {
        root * /var/www/crypto-dashboard
        file_server
    }
}

Load Balancing ile WebSocket

Birden fazla backend sunucunuz varsa ve bunlar arasında load balancing yapmak istiyorsanız, WebSocket bağlantıları için dikkatli olmanız gerekiyor. WebSocket bağlantısı kurulduktan sonra aynı backend üzerinde kalmak zorunda. Caddy bunu sticky sessions veya doğru load balancing politikasıyla çözüyor.

websocket-farm.example.com {
    reverse_proxy {
        to localhost:3001 localhost:3002 localhost:3003

        # IP hash ile aynı kullanıcı hep aynı backend'e gider
        lb_policy ip_hash

        # Sağlık kontrolü
        health_uri /health
        health_interval 15s
        health_timeout 5s
        health_status 200

        # Pasif sağlık kontrolü
        fail_duration 30s
        max_fails 3
        unhealthy_status 429 500 502 503 504
    }
}

lb_policy ip_hash: Aynı IP adresinden gelen tüm istekleri aynı backend’e yönlendirir. WebSocket için ideal çünkü bağlantı kurulduktan sonra kullanıcı aynı sunucuya bağlı kalır.

Alternatif load balancing politikaları:

  • round_robin: Sırayla her backend’e istek gönderir. Stateless HTTP için iyi ama WebSocket için sorunlu.
  • least_conn: En az aktif bağlantısı olan backend’e yönlendirir. Yük dengeleme açısından adil.
  • random: Rastgele seçim. Basit ama WebSocket için pek uygun değil.
  • first: Sağlıklı ilk backend’i kullanır. Failover senaryoları için.
  • ip_hash: WebSocket için en mantıklı seçenek.

Redis ile Session Paylaşımı Senaryosu

Gerçek dünya uygulamalarında birden fazla WebSocket sunucusu çalıştırıyorsanız ve kullanıcı state’ini yönetmeniz gerekiyorsa, genellikle Redis gibi bir pub/sub sistemi kullanılıyor. Caddy tarafında yapılandırma şöyle görünüyor:

# Birden fazla Node.js WebSocket sunucusu
# Redis üzerinden birbirleriyle konuşuyor
# Caddy bunları load balance ediyor

chat.example.com {
    reverse_proxy /socket.io/* {
        to 10.0.0.1:3000 10.0.0.2:3000 10.0.0.3:3000

        lb_policy ip_hash

        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        health_uri /health
        health_interval 20s
        health_timeout 5s
    }

    handle {
        root * /var/www/chat
        file_server
    }
}

TLS ve WSS Yapılandırması

Caddy’nin en büyük avantajlarından biri TLS sertifikalarını otomatik yönetmesi. Bir domain tanımladığınızda Caddy Let’s Encrypt’ten sertifika alıyor ve yeniliyor. WebSocket bağlantıları wss:// protokolü üzerinden güvenli şekilde çalışıyor.

# Caddy TLS'yi otomatik hallediyor
# İstemci wss://realtime.example.com/ws bağlantısı kuruyor
# Caddy TLS'yi sonlandırıyor ve ws://localhost:3000/ws olarak iletiyor

realtime.example.com {
    # TLS otomatik, ekstra yapılandırmaya gerek yok

    log {
        output file /var/log/caddy/realtime-access.log {
            roll_size 100mb
            roll_keep 10
        }
        format json
    }

    reverse_proxy /ws {
        to localhost:3000

        header_up X-Forwarded-Proto {scheme}
        header_up X-Real-IP {remote_host}
    }
}

Eğer özel sertifika kullanmak istiyorsanız:

realtime.example.com {
    tls /etc/ssl/certs/realtime.crt /etc/ssl/private/realtime.key

    reverse_proxy /ws localhost:3000 {
        header_up X-Forwarded-Proto {scheme}
    }
}

Socket.IO Özel Yapılandırması

Socket.IO yaygın kullanılan bir WebSocket kütüphanesi ve bazı özel davranışları var. Hem HTTP long-polling hem de WebSocket kullanabiliyor. Caddy ile Socket.IO kullanıyorsanız path matching’e dikkat etmek gerekiyor:

socketio-app.example.com {
    # Socket.IO hem /socket.io/ path'ini hem de WebSocket hem de HTTP polling kullanıyor
    reverse_proxy /socket.io/* localhost:3000 {
        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Socket.IO polling için timeout biraz daha uzun olabilir
        transport http {
            response_header_timeout 60s
        }
    }

    handle {
        root * /var/www/app
        file_server
    }
}

Socket.IO v4 ile Caddy’yi test etmek için hızlı bir Node.js script:

# Test için basit bir WebSocket sunucusu başlatmak
# node ws-test-server.js

cat > /tmp/ws-test.js << 'EOF'
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000, path: '/ws' });

wss.on('connection', (ws, req) => {
    const clientIP = req.headers['x-real-ip'] || req.socket.remoteAddress;
    console.log(`Yeni bağlantı: ${clientIP}`);

    ws.send(JSON.stringify({ type: 'welcome', message: 'Caddy üzerinden bağlandınız!' }));

    ws.on('message', (data) => {
        console.log(`Mesaj alındı: ${data}`);
        ws.send(JSON.stringify({ type: 'echo', data: data.toString() }));
    });

    ws.on('close', () => {
        console.log(`Bağlantı kapandı: ${clientIP}`);
    });
});

console.log('WebSocket sunucusu port 3000 üzerinde çalışıyor...');
EOF

node /tmp/ws-test.js &

Bağlantıyı test etmek için:

# wscat ile test
npm install -g wscat
wscat -c wss://realtime.example.com/ws

# Veya websocat ile
websocat wss://realtime.example.com/ws

Hata Ayıklama ve Loglama

WebSocket bağlantılarında sorun yaşıyorsanız Caddy’nin log sistemi çok işe yarıyor. Detaylı loglama için:

{
    # Global log seviyesi
    debug
}

realtime.example.com {
    log {
        output file /var/log/caddy/ws-debug.log
        level DEBUG
        format json
    }

    reverse_proxy /ws localhost:3000
}

Caddy servisini yeniden başlatıp logları izlemek için:

# Caddy yapılandırmasını test et
caddy validate --config /etc/caddy/Caddyfile

# Reload yap (downtime olmadan)
caddy reload --config /etc/caddy/Caddyfile

# Logları canlı izle
tail -f /var/log/caddy/ws-debug.log | jq .

# Sadece WebSocket bağlantılarını filtrele
tail -f /var/log/caddy/ws-debug.log | jq 'select(.request.headers.Upgrade != null)'

Sık karşılaşılan sorunlar ve çözümleri:

  • 502 Bad Gateway: Backend çalışmıyor veya port yanlış. ss -tlnp | grep 3000 ile kontrol edin.
  • 400 Bad Request: WebSocket handshake başarısız. Header’ların doğru iletilip iletilmediğini kontrol edin.
  • Connection timeout: Backend yavaş yanıt veriyor veya firewall blokluyor. dial_timeout değerini artırın.
  • WebSocket connection closed: Backend çöktü veya bağlantı limiti aşıldı. max_conns_per_host değerini kontrol edin.

Rate Limiting ile WebSocket Koruması

WebSocket bağlantıları potansiyel olarak kötüye kullanılabilir. Her bağlantı sunucu kaynaklarını tüketiyor. Caddy’nin rate limiting plugin’i ile bunu kontrol altına alabilirsiniz:

{
    order rate_limit before reverse_proxy
}

realtime.example.com {
    rate_limit {
        zone ws_connections {
            key {remote_host}
            events 10
            window 1m
        }
    }

    reverse_proxy /ws localhost:3000 {
        header_up X-Real-IP {remote_host}
        max_conns_per_host 500
    }
}

Rate limiting plugin’ini yüklemek için:

# xcaddy ile rate limiting modülü ekleyerek derle
xcaddy build --with github.com/mholt/caddy-ratelimit

# Veya resmi xcaddy ile
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build 
    --with github.com/mholt/caddy-ratelimit 
    --output /usr/local/bin/caddy

Monitoring ve Metrik Toplama

Üretim ortamında WebSocket bağlantı sayısını, bağlantı sürelerini ve hata oranlarını izlemek kritik. Caddy’nin Prometheus metrik endpoint’ini aktif edelim:

{
    metrics
}

realtime.example.com {
    reverse_proxy /ws localhost:3000

    handle /metrics {
        # Sadece dahili ağdan erişim izni ver
        @internal {
            remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
        }
        handle @internal {
            metrics
        }
        respond 403
    }
}

Prometheus scrape config:

# prometheus.yml
scrape_configs:
  - job_name: 'caddy'
    static_configs:
      - targets: ['realtime.example.com:2019']
    metrics_path: '/metrics'

Caddy admin API ile anlık bağlantı durumuna bakmak için:

# Admin API varsayılan olarak localhost:2019'da çalışır
curl http://localhost:2019/metrics | grep caddy_reverse_proxy

# Aktif upstream'leri listele
curl http://localhost:2019/config/ | jq '.apps.http.servers'

Gerçek Dünya Örneği: Online Oyun Sunucusu

Bir online tahta oyunu düşünün. Oyuncular eşleşiyor, gerçek zamanlı hamle gönderiyor, chat yapıyor. Birden fazla oyun sunucusu var ve her sunucu belirli sayıda aktif oyun yönetiyor.

{
    email [email protected]
}

game.example.com {
    encode gzip

    log {
        output file /var/log/caddy/game-access.log {
            roll_size 50mb
            roll_keep 7
        }
        format json
    }

    # Oyun WebSocket bağlantıları
    reverse_proxy /game/ws/* {
        to 10.0.1.10:4000 10.0.1.11:4000 10.0.1.12:4000

        lb_policy ip_hash

        health_uri /health
        health_interval 10s
        health_timeout 3s
        health_status 200

        fail_duration 60s
        max_fails 2

        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}

        transport http {
            dial_timeout 5s
            response_header_timeout 15s
            keepalive 60s
        }
    }

    # Eşleştirme API'si
    reverse_proxy /api/* 10.0.1.20:8080 {
        header_up X-Real-IP {remote_host}
    }

    # Statik dosyalar
    handle {
        root * /var/www/boardgame
        file_server
    }
}

Bu yapılandırmada üç ayrı oyun sunucusu var. IP hash ile aynı oyuncu hep aynı sunucuya bağlanıyor. Sunucu düşerse Caddy otomatik olarak diğerine yönlendiriyor. Sağlık kontrolü her 10 saniyede bir yapılıyor, 2 ardışık başarısızlıkta sunucu 60 saniyeliğine devre dışı bırakılıyor.

Sonuç

Caddy, WebSocket proxy yapılandırmasını gerçekten basitleştiriyor. Nginx’te el ile yazmak zorunda kaldığımız proxy_http_version 1.1, proxy_set_header Upgrade, proxy_set_header Connection satırları Caddy ile tamamen ortadan kalkıyor. Otomasyon sadece burada bitmiyor; TLS sertifika yönetimi, otomatik HTTPS yönlendirmesi, akıllı varsayılan güvenlik ayarları da işin içinde geliyor.

Üretime alırken şunlara dikkat etmek gerekiyor: Load balancing yapıyorsanız ip_hash politikasını tercih edin. Backend timeout değerlerini uygulamanızın beklenen bağlantı süresine göre ayarlayın. Rate limiting’i ihmal etmeyin çünkü açık WebSocket bağlantıları saldırı vektörü olabilir. Loglama ve monitoring’i baştan kurun, sorun çıktığında neyin ne zaman olduğunu görebilmek paha biçilmez.

Caddy’nin dokümantasyonu oldukça iyi ama bazen özellikle transport http bloğu içindeki gelişmiş ayarlar için biraz araştırma yapmak gerekebiliyor. Topluluk forumu ve GitHub discussions bu boşluğu dolduruyor. Nginx’ten geçiş yapıyorsanız başlangıçta her şeyi elle yazmadığınız için garip hissedebilirsiniz; ama zamanla bu basitliğin ne kadar değerli olduğunu anlayacaksınız.

Yorum yapın