Mert Özen Her Satırda Daha İleri
DevOps Web Server'lar ve Hosting — #01

Web Server, Application Server ve Reverse Proxy: Bir HTTP İsteği Uygulamana Nasıl Ulaşır?

Mert Özen 8 Kas 2026 10 dk 5 görüntülenme
Web Server, Application Server ve Reverse Proxy: Bir HTTP İsteği Uygulamana Nasıl Ulaşır?

Kestrel hem web server hem application server; reverse proxy ise isteği karşılayıp arkadaki uygulamaya ileten, TLS termination, statik dosya, compression, buffering ve load balancing'i üstlenen katman. Aynı isteği önce doğrudan Kestrel'e, sonra Nginx üzerinden gönderip uygulamanın gördüğü IP'yi, header'ları ve Angular deep link davranışını karşılaştırıyor, beş hosting topolojisini yan yana koyuyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (ASP.NET Core runtime 10.0.14), Nginx 1.30.6, Angular 22.2 ve curl 8.23 ile Docker 29 üzerinde denenmiştir.

Önce Tahmin Et

Bookstore'un API'si (Application Programming Interface) ilk kez bir Linux VM'e (virtual machine) çıkıyor. Ekipten biri "Kestrel zaten bir web server, 80 portunu doğrudan ona verelim" diyor; bir diğeri "önüne Nginx koyalım" diyor. Aynı GET /api/whoami isteğini önce doğrudan Kestrel'e, sonra Nginx üzerinden gönderiyoruz. Uygulama her iki durumda da isteği kimin gönderdiğini, hangi Host ile geldiğini ve yanıtın hangi sunucudan çıktığını söylüyor.

Tahmin et: Nginx üzerinden giden istekte uygulama client'ın IP (Internet Protocol) adresini mi görür, Nginx'inkini mi? Yanıttaki Server header'ında ne yazar? Ve Angular'ın /orders/42 sayfasını tarayıcıda yenileyen kullanıcı, iki kurulumda aynı sonucu mu alır?

Üç Rol: Web Server, Application Server, Reverse Proxy

Bu üç kelime sık sık birbirinin yerine kullanılıyor, ama farklı işleri anlatıyorlar. Bir isteğin yolunu doğru okumak için önce rolleri ayırmak gerekiyor.

Web server, HTTP (Hypertext Transfer Protocol) konuşan, port dinleyen, bağlantıyı kabul edip isteği parse eden ve yanıtı geri yazan yazılım. Nginx, IIS (Internet Information Services), Apache ve Kestrel'in hepsi bu tanıma uyuyor. Application server ise senin kodunu çalıştıran katman: ASP.NET Core'da request pipeline'ı, middleware'ler, routing ve endpoint'ler. .NET dünyasında bu iki rol çoğu zaman aynı process içinde birleşiyor: Kestrel hem portu dinliyor hem de isteği doğrudan senin middleware pipeline'ına veriyor. Java tarafındaki Tomcat ya da Python'daki Gunicorn da benzer biçimde "uygulamayı çalıştıran HTTP sunucusu" rolünde.

Reverse proxy, client'tan gelen isteği karşılayıp arkadaki bir ya da birden çok sunucuya ileten aracı. Client yalnızca proxy'yi görür; arkada kaç Kestrel instance'ı olduğunu, hangi portta çalıştıklarını bilmez. "Reverse" kelimesi, kurumsal ağlarda client tarafında duran forward proxy'den ayırmak için: forward proxy client'ları temsil eder, reverse proxy sunucuları.

Aynı yazılım birden çok rol oynayabilir. Nginx bu yazıdaki kurulumda hem web server (Angular'ın derlenmiş dosyalarını diskten veriyor) hem reverse proxy (/api/ isteklerini Kestrel'e iletiyor). Kestrel ise hem web server hem application server.

Bir İsteğin Yolculuğu: curl ile Katmanları Görmek

Deney düzeneği küçük: Docker'da bir network, 172.30.85.21 adresinde Bookstore.Api'yi çalıştıran bir Kestrel container'ı, 172.30.85.10 adresinde Nginx ve isteği gönderen 172.30.85.50 adresli bir client container'ı. API'deki /api/whoami endpoint'i, isteğin uygulamaya nasıl ulaştığını JSON (JavaScript Object Notation) olarak döndürüyor:

app.MapGet("/api/whoami", (HttpContext ctx) => new
{
    Instance = instance,
    RemoteIp = ctx.Connection.RemoteIpAddress?.ToString(),
    RemotePort = ctx.Connection.RemotePort,
    ConnectionId = ctx.Connection.Id,
    ctx.Request.Protocol,
    ctx.Request.Scheme,
    Host = ctx.Request.Host.Value,
    PathBase = ctx.Request.PathBase.Value,
    Path = ctx.Request.Path.Value,
    XForwardedFor = ctx.Request.Headers["X-Forwarded-For"].ToString(),
    XOriginalFor = ctx.Request.Headers["X-Original-For"].ToString(),
    XForwardedProto = ctx.Request.Headers["X-Forwarded-Proto"].ToString(),
    Link = $"{ctx.Request.Scheme}://{ctx.Request.Host}{ctx.Request.PathBase}/api/books/1",
});

Önce doğrudan Kestrel'e:

$ curl -si http://172.30.85.21:8080/api/books/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 08 Nov 2026 03:28:16 GMT
Server: Kestrel
Transfer-Encoding: chunked

{"id":1,"title":"Dune","author":"Frank Herbert","price":12.50}

Yanıtı Kestrel üretiyor ve bunu Server: Kestrel header'ıyla söylüyor. Şimdi aynı API'nin önüne Nginx koyuyoruz. Config'in tamamı bu kadar:

server {
    listen 80;
    server_name bookstore.local;

    gzip on;
    gzip_types application/json;

    # Angular build'i: dosyaları Nginx doğrudan diskten verir
    location / {
        root /srv/bookstore-web;
        try_files $uri $uri/ /index.html;
    }

    # API: Nginx isteği Kestrel'e iletir
    location /api/ {
        proxy_pass http://172.30.85.21:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Aynı client'tan, bu kez Nginx'e. Önce yalnız yanıt header'larına, gzip kabul ettiğimizi söyleyerek; sonra JSON gövdesine bakıyoruz:

$ curl -s -o /dev/null -D - --compressed -H "Host: bookstore.local" http://172.30.85.10/api/whoami
HTTP/1.1 200 OK
Server: nginx/1.30.6
Date: Sun, 08 Nov 2026 03:28:27 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip

$ curl -s -H "Host: bookstore.local" http://172.30.85.10/api/whoami | jq .
{
  "instance": "api1",
  "remoteIp": "::ffff:172.30.85.10",
  "remotePort": 59598,
  "connectionId": "0HNPT8URF4Q5V",
  "protocol": "HTTP/1.1",
  "scheme": "http",
  "host": "bookstore.local",
  "pathBase": "",
  "path": "/api/whoami",
  "xForwardedFor": "172.30.85.50",
  "xOriginalFor": "",
  "xForwardedProto": "http",
  "link": "http://bookstore.local/api/books/1"
}

Tahminin cevabı burada. Uygulamanın gördüğü RemoteIp artık client'ın değil, Nginx'in adresi (172.30.85.10; başındaki ::ffff:, Kestrel'in IPv4 adresini IPv6 biçiminde göstermesi). Client'ın gerçek adresi yalnızca Nginx'in eklediği X-Forwarded-For header'ında duruyor. Server header'ı da değişti: client artık Kestrel'i hiç görmüyor, yanıtı Nginx gönderiyor ve gzip ile sıkıştırmayı da Nginx yaptı. Kestrel düz JSON üretti; Content-Encoding: gzip header'ı Nginx'in eseri.

Bir header'ın varlığı, uygulamanın onu kullanacağı anlamına gelmiyor. Request.Scheme ve RemoteIpAddress hâlâ Nginx ile Kestrel arasındaki bağlantıyı anlatıyor; bunları X-Forwarded-* değerlerine göre düzeltmek ASP.NET Core'daki forwarded headers middleware'inin işi ve serinin üçüncü yazısının konusu.

Sorunun üçüncü kısmı Angular. Tarayıcıda /orders/42 sayfasını yenileyen kullanıcı için iki kurulum farklı davranıyor:

$ curl -sI -H "Host: bookstore.local" http://172.30.85.10/orders/42
HTTP/1.1 200 OK
Server: nginx/1.30.6
Date: Sun, 08 Nov 2026 03:28:17 GMT
Content-Type: text/html
Content-Length: 528
Last-Modified: Sun, 08 Nov 2026 03:22:48 GMT
Connection: keep-alive
ETag: "6aefeb08-210"
Accept-Ranges: bytes

$ curl -s -o /dev/null -w "%{http_code}\n" http://172.30.85.21:8080/orders/42
404

Diskte orders/42 diye bir dosya yok. Nginx try_files ile index.html'i döndürüyor, Angular router da route'u tarayıcıda çözüyor. Kestrel'deki API bu path'i tanımadığı için 404 veriyor. Aynı işi ASP.NET Core'da UseStaticFiles ve MapFallbackToFile ile de yapabilirsin; nokta şu ki "statik dosya ve SPA (Single Page Application) fallback" bir sorumluluk ve birinin üstlenmesi gerekiyor. Nginx tarafındaki ayrıntılar altıncı yazıda.

Reverse Proxy'nin Üstlendiği İşler

Yukarıdaki on satırlık config bile birkaç işi Kestrel'in üstünden aldı. Reverse proxy'nin tipik sorumlulukları şöyle:

SorumlulukReverse proxy'deKestrel'de karşılığı
TLS (Transport Layer Security) terminationCertificate tek yerde, Kestrel'e düz HTTPKestrel de TLS yapabilir; her instance'a certificate gerekir
Statik dosyaroot, try_files, sendfileUseStaticFiles, MapStaticAssets
Compressiongzip, önceden sıkıştırılmış dosya için gzip_staticUseResponseCompression
Request ve response bufferingYavaş client'ın body'sini önce Nginx toplarKestrel her bağlantıyı kendisi bekler
Load balancingupstream ile birden çok instanceYok; tek process
Koruma katmanıRate limiting, bağlantı limiti, bilinmeyen Host'u reddetmeAddRateLimiter, host filtering
Port paylaşımıAynı 443'te birden çok domain ve uygulamaBir port, bir process

Buffering, listede en az bilinen ama en çok iş gören madde. Mobil ağdaki bir kullanıcı 10 MB'lık dosyayı 30 saniyede yüklüyorsa, proxy olmadan o 30 saniye boyunca Kestrel'de bir bağlantı ve request body'yi okuyan bir istek açık kalır. Nginx varsayılan olarak request body'yi kendisi toplar (proxy_request_buffering on) ve Kestrel'e ancak tamamı geldiğinde, yerel ağın hızında iletir. Yanıt tarafında da aynısı geçerli (proxy_buffering on): Kestrel yanıtı hızla Nginx'e bırakır ve işine döner, yavaş client'a damla damla yazmak Nginx'in derdi olur. Bu davranışın bedelini Server-Sent Events gibi akış yapan endpoint'lerde ödersin; beşinci yazıda göreceğiz.

Port paylaşımı ise Microsoft Learn'deki Kestrel dokümanında açıkça yazan bir sınır: Kestrel edge'de tek başına çalışırken aynı IP ve portu birden çok process ile paylaşamaz; bir portu dinlediğinde o porttaki bütün trafiği Host header'ına bakmadan alır. Aynı sunucuda api.bookstore.example ile admin.bookstore.example'ı iki ayrı uygulama olarak 443'ten yayınlamak istiyorsan, önde host'a göre dağıtan bir şey gerekiyor.

Kestrel Tek Başına Ne Zaman Yeterli?

Microsoft'un tutumu net: Kestrel'i edge'de tek başına çalıştırmak da reverse proxy arkasında çalıştırmak da desteklenen kurulumlar. Kestrel HTTPS (HTTP Secure), HTTP/2, HTTP/3, request limit'leri ve timeout'larla internete dönük çalışacak şekilde sertleştirilmiş durumda. "Kestrel production'a hazır değil, mutlaka önüne bir şey koy" tavsiyesi ASP.NET Core 1.x yıllarından kalma; bugün geçerli değil.

Kestrel'in tek başına yettiği tipik durumlar:

  • Sunucuda tek uygulama var, tek port yeterli ve certificate'ı Kestrel'e vermek seni rahatsız etmiyor.
  • Önünde zaten bir cloud load balancer ya da gateway var; TLS termination ve dağıtımı o yapıyor (Azure App Service'in front end'i, AWS (Amazon Web Services) ALB (Application Load Balancer), Kubernetes ingress).
  • İç ağdaki bir servis; client'lar başka servisler ve trafiği zaten bir gateway topluyor.

Proxy'yi gerekli kılanlar ise yukarıdaki tablonun sağ sütununda "yok" ya da "her instance'a" yazan satırlar: aynı makinede birden çok uygulama, birden çok instance arasında yük dağıtımı, certificate'ı tek yerde yönetmek, Angular gibi bir SPA'yı aynı origin'den sunmak, ekibin zaten bildiği ve izlediği bir edge katmanı.

Beş Hosting Topolojisi

Bir .NET uygulamasını yayınlamanın beş yaygın şekli var. Seri boyunca hepsine tek tek gireceğiz; burada büyük resmi görelim.

01-hosting-topologies-tr.png

Kestrel tek başına: client doğrudan Kestrel'e bağlanır, TLS'i Kestrel yapar. En az parça, en az esneklik.

Nginx ve Kestrel: Linux VM'deki klasik kurulum. Nginx 80 ve 443'ü dinler, TLS'i sonlandırır, statik dosyaları verir ve /api'yi 127.0.0.1:5000 gibi yerel bir adresteki Kestrel'e iletir. Kestrel'i systemd ayakta tutar. Serinin dördüncü ile sekizinci yazıları bu kurulumu adım adım kuruyor.

IIS ve ASP.NET Core Module: Windows Server'daki karşılığı. ANCM (ASP.NET Core Module) IIS'e takılan native bir modül; uygulamayı ya IIS worker process'inin (w3wp.exe) içinde çalıştırır (in-process, Kestrel yerine IIS HTTP Server kullanılır) ya da ayrı bir process'te Kestrel'i başlatıp isteği ona iletir (out-of-process). Dokuzuncu ve onuncu yazıların konusu.

Cloud load balancer ve Kestrel: Azure App Service, AWS ALB ya da bir cloud load balancer TLS'i ve dağıtımı üstlenir, VM'lerdeki ya da container'lardaki Kestrel'e iletir. Proxy yine var; sadece senin yönettiğin bir yazılım değil.

Container ve ingress ya da gateway: Uygulama container'da 8080'i dinleyen Kestrel; önünde Kubernetes ingress controller, Gateway API ya da bir API gateway. Bu topolojinin kendisi ilerideki Docker ve Kubernetes serilerinde; burada önemli olan, reverse proxy kavramının değişmediği: isteği alan ile işleyen yine farklı process'ler, X-Forwarded-* header'ları yine gerekli.

Üretimde Dikkat

Hangi topolojiyi seçersen seç, isteği kimin sonlandırdığını bir kez yazılı hâle getir. "TLS nerede bitiyor, client IP'si hangi header'da geliyor, timeout hangi katmanda kaç saniye" sorularının cevabı dağınıksa, ilk 502 ya da 504'te üç ekip birbirine bakar. Bookstore için bu tablo üç satır: TLS Nginx'te biter, client IP'si X-Forwarded-For'da gelir, Nginx'in upstream timeout'u 60 saniye ve Kestrel'in request header timeout'u 30 saniye.

Her katman kendi log'unu tutar ve bir istek her katmanda ayrı satır üretir. Nginx'in access log'u yukarıdaki istekleri şöyle kaydetti:

172.30.85.50 - - [08/Nov/2026:03:28:27 +0000] "GET /api/whoami HTTP/1.1" 200 237 "-" "curl/8.23.0" "-"
172.30.85.50 - - [08/Nov/2026:03:28:27 +0000] "GET /api/whoami HTTP/1.1" 200 330 "-" "curl/8.23.0" "-"

İki satır aynı endpoint'e gitti ama boyutlar farklı: 237 byte, gzip isteyen ilk çağrının sıkıştırılmış gövdesi; 330 byte, sıkıştırma istemeyen ikinci çağrının düz JSON'u. Kestrel'in log'unda ikisi de aynı boyutta görünür. İki log'u eşleştirmek için zaman damgası yetmez; correlation ID ya da W3C (World Wide Web Consortium) trace context taşıman gerekir. .NET'te Logging serisinin "Correlation ID ve Trace Context" yazısı bu bağlantıyı anlatıyor.

Katman eklemek gecikme de ekler, ama sanıldığı kadar değil. Aynı makinedeki Nginx'ten Kestrel'e loopback üzerinden gitmek mikro saniyeler mertebesinde; asıl maliyet yanlış ayarlanmış buffer'lar ve keepalive'ın kapalı olması yüzünden her istekte yeni TCP (Transmission Control Protocol) bağlantısı açılması. Bu ikisi de beşinci yazıda ölçülüyor.

Sık Düşülen Tuzaklar

Kestrel'i hem Nginx'in arkasına koyup hem de dışarıya açık bırakmak. Kestrel 0.0.0.0:5000'i dinliyorsa ve firewall kapalı değilse, proxy'yi atlayıp doğrudan Kestrel'e gelen biri rate limiting'den de, Nginx'in güvenlik header'larından da kurtulur. Proxy arkasındaki Kestrel 127.0.0.1'i ya da bir Unix socket'i dinlemeli.

Proxy'nin eklediği header'ın uygulamada kendiliğinden kullanıldığını sanmak. Deneyde gördüğün gibi X-Forwarded-For geldi, ama RemoteIpAddress hâlâ Nginx'in adresi. IP'ye göre rate limiting yapan bir uygulama bu hâliyle bütün kullanıcıları tek bir client sayar.

Aynı işi iki katmanda yapmak. Hem Nginx'te gzip hem Kestrel'de UseResponseCompression açıksa CPU (Central Processing Unit) iki kez harcanmaz ama config iki yerde yaşar ve biri değiştiğinde diğeri unutulur. HSTS (HTTP Strict Transport Security) header'ını iki katmanın da eklemesi ise yanıtta iki ayrı header'a yol açar. Her sorumluluk için tek bir sahip seç.

"Kestrel internete çıkamaz" inancıyla gereksiz katman eklemek. Önünde zaten cloud load balancer olan bir uygulamanın VM'ine bir de Nginx koymak, bir hop, bir config ve bir güncelleme takvimi daha demek. Proxy'yi bir ihtiyaç için ekle.

Küçük Bir Deneme

★☆☆ Deneydeki Nginx config'inden proxy_set_header Host $host; satırını sil, config'i yeniden yükle ve /api/whoami'yi tekrar çağır. host ve link alanları neye dönüştü? Bu değerle üretilen bir e-posta doğrulama linki kullanıcıyı nereye götürür?

★★☆ Client container'ından X-Forwarded-For: 1.2.3.4 header'ını kendin ekleyerek isteği Nginx'e gönder. xForwardedFor alanında kaç adres görüyorsun ve hangisi gerçekten client'ın? $proxy_add_x_forwarded_for yerine $remote_addr kullanınca sonuç nasıl değişiyor?

★★★ Kestrel'in yanıtındaki Server: Kestrel header'ını KestrelServerOptions.AddServerHeader = false ile kapat, Nginx'te de server_tokens off; ekle. İki ayardan sonra bir saldırgan yanıta bakarak arkada hangi teknolojilerin çalıştığını hâlâ çıkarabilir mi? Hata sayfalarını (502, 404) da kontrol et.

Bu yazıda Kestrel'i hep varsayılan hâliyle, container'ın verdiği 8080 portunda çalıştırdık. Bir sonraki yazıda Kestrel'in kendisine giriyoruz: hangi port'u neden dinlediği, --urls, ASPNETCORE_URLS, ASPNETCORE_HTTP_PORTS ve Kestrel:Endpoints aynı anda verildiğinde hangisinin kazandığı, certificate ve limit ayarları, HTTP/2 ile HTTP/3.