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

.NET 6'dan .NET 10'a Hosting'de Ne Değişti: Port 8080, HTTP_PORTS, KnownIPNetworks ve Diğerleri

Mert Özen 8 Kas 2026 9 dk 6 görüntülenme
.NET 6'dan .NET 10'a Hosting'de Ne Değişti: Port 8080, HTTP_PORTS, KnownIPNetworks ve Diğerleri

.NET 8'den beri ASP.NET Core image'ları 80 yerine 8080'i dinliyor ve hazır bir app kullanıcısı getiriyor, ama varsayılan kullanıcı hâlâ root; .NET 10'da image'lar Ubuntu tabanlı ve KnownNetworks ile WebHost obsolete. Eski -p 8000:80 komutunun connection reset vermesini, Docker'da root olmayan 80'in neden çalışıp başka runtime'da çalışmayabileceğini, eski tutorial'lardaki tuzakları ve .NET 11'in hosting'e dokunan breaking change'lerini gerçek koşularla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 SDK (Software Development Kit) 10.0.402 ve mcr.microsoft.com/dotnet/aspnet image'larının 6.0, 8.0 ve 10.0 etiketleriyle Docker 29 üzerinde denenmiştir. .NET 11'in GA (General Availability) tarihi 10 Kasım 2026; bu yazı yazılırken henüz yayında değildi, .NET 11 maddeleri yalnız Microsoft'un yayımladığı breaking change sayfalarına dayanıyor.

Önce Tahmin Et

Bookstore ekibi 2021'de yazılmış bir blog yazısındaki Dockerfile'ı ve docker run komutunu kullanıyor: docker run -p 18581:80 .... Tek fark, image'ın artık mcr.microsoft.com/dotnet/aspnet:10.0 olması. Container ayağa kalkıyor, log'da hata yok.

Tahmin et: curl http://localhost:18581/api/books/1 ne döner? Ve ekip güvenlik taramasından gelen "container root ile çalışıyor" bulgusunu kapatmak için --user app ekleyip uygulamayı 80'i dinleyecek şekilde ayarlarsa ne olur?

Container'da 80'den 8080'e

.NET 6'dan 10'a hosting'deki en çok iz bırakan değişiklik bir ortam değişkeni. Üç image'ın varsayılanları:

$ for v in 6.0 8.0 10.0; do docker run --rm mcr.microsoft.com/dotnet/aspnet:$v sh -c '...'; done
== aspnet:6.0
ASPNETCORE_URLS=http://+:80
user: root (0)
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
id: 'app': no such user
== aspnet:8.0
APP_UID=1654
ASPNETCORE_HTTP_PORTS=8080
user: root (0)
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
uid=1654(app) gid=1654(app) groups=1654(app)
== aspnet:10.0
APP_UID=1654
ASPNETCORE_HTTP_PORTS=8080
user: root (0)
PRETTY_NAME="Ubuntu 24.04.5 LTS"
uid=1654(app) gid=1654(app) groups=1654(app)

.NET 8'den beri image'lar ASPNETCORE_URLS=http://+:80 yerine ASPNETCORE_HTTP_PORTS=8080 ile geliyor. Tahminin cevabı:

$ docker run -d -p 18581:80 ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
$ curl -sS http://localhost:18581/api/books/1
curl: (56) Recv failure: Connection reset by peer

$ docker run -d -p 18581:8080 ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
$ curl -sS http://localhost:18581/api/books/1
{"id":1,"title":"Dune","author":"Frank Herbert","price":12.50}

Container'ın içinde 80'i kimse dinlemiyor; Docker bağlantıyı kabul edip kapatıyor. Microsoft'un breaking change dokümanındaki örnek de birebir bu. Bir ayrıntı daha: eski WebHost.CreateDefaultBuilder API'si (Application Programming Interface) yeni ASPNETCORE_HTTP_PORTS'u tanımıyor ve ASPNETCORE_URLS da artık set edilmediği için bu tür uygulamalar container'da http://localhost:5000'e düşüyor. İkinci yazıdaki localhost tuzağının aynısı: container içinde ayakta, dışarıdan erişilemez.

Non-Root app Kullanıcısı: Opt-In

Çıktıda dikkat çeken ikinci satır APP_UID=1654 ve app kullanıcısı. Sık okunan bir yanlış, .NET 8'den beri container'ların varsayılan olarak root olmadan çalıştığı. Çıktının gösterdiği gibi üç image'da da varsayılan kullanıcı root; .NET 8 yalnız hazır bir app kullanıcısı ekledi, onu kullanmak senin kararın (Dockerfile'da USER $APP_UID ya da docker run --user app). 8080'e geçişin gerekçelerinden biri de bu: geleneksel olarak 1024'ün altındaki portları yalnız root açabiliyor.

$ docker run -d --user app ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
$ docker exec webhwork-port ps -o user,pid,args
USER         PID COMMAND
app            1 dotnet Bookstore.Api.dll
$ docker logs webhwork-port
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/home/app/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. ...
      Now listening on: http://[::]:8080

Tahminin ikinci yarısı beklenmedik çıktı: --user app ile ASPNETCORE_HTTP_PORTS=80 Docker'da çalıştı. Sebep Docker'ın kendisi; container'larda net.ipv4.ip_unprivileged_port_start'ı 0 yapıyor ve root olmayan process'ler de 80'i açabiliyor:

$ docker run -d --user app -e ASPNETCORE_HTTP_PORTS=80 ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
      Now listening on: http://[::]:80
$ docker exec webhwork-port cat /proc/sys/net/ipv4/ip_unprivileged_port_start
0

$ docker run -d --user app --sysctl net.ipv4.ip_unprivileged_port_start=1024 -e ASPNETCORE_HTTP_PORTS=80 ...
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
      Hosting failed to start
      System.Net.Sockets.SocketException (13): Permission denied

Aynı image Kubernetes'te, Podman'da ya da sysctl'i farklı bir runtime'da "Permission denied" ile açılmayabilir. 8080'de kalmak bu farkı tamamen ortadan kaldırıyor; dış port eşleşmesi zaten ingress'in ya da -p'nin işi. Container hardening'in geri kalanı ilerideki Docker serisinde.

ASPNETCORE_HTTP_PORTS ve Port Önceliği

HTTP_PORTS ve HTTPS_PORTS .NET 8 ile geldi ve ikinci yazıda ölçtüğümüz öncelik sırasının en altında duruyor: urls anahtarı (--urls, ASPNETCORE_URLS) onları eziyor, Kestrel:Endpoints ve koddaki Listen* de urls'ü. Eski bir Dockerfile'da ENV ASPNETCORE_URLS=http://+:80 satırı kaldıysa bu satır image'ın 8080'ini sessizce eziyor ve uygulama yine 80'i dinliyor; log'da tek ipucu Overriding HTTP_PORTS '8080' and HTTPS_PORTS '' uyarısı. İki dünyayı karıştırma: ya eski satırı sil ve 8080'e geç, ya da bilinçli olarak tut ve port eşleşmelerini ona göre yaz.

.NET 10: Ubuntu Tabanlı Image'lar ve Obsolete API'ler

.NET 10'da varsayılan image'ların tabanı Debian'dan Ubuntu 24.04'e geçti; yukarıdaki PRETTY_NAME satırı bunu gösteriyor. Microsoft'un dokümanına göre .NET 10 için Debian image'ı yayımlanmıyor. Dockerfile'ında apt-get install ile paket kuran ekipler için paket adları ve sürümleri değişebilir; ikinci yazıdaki libmsquic örneğinde Ubuntu 24.04'ün Microsoft depo adresini kullandık.

Kod tarafında iki obsolete uyarısı var. Birincisi üçüncü yazıda gördüğümüz KnownNetworks:

Program.cs(9,5): warning ASPDEPR005: 'ForwardedHeadersOptions.KnownNetworks' is obsolete: 'Please use KnownIPNetworks instead. For more information, visit https://aka.ms/aspnet/deprecate/005.' (https://aka.ms/aspnet/deprecate/005)
Program.cs(9,35): warning ASPDEPR005: 'IPNetwork' is obsolete: 'Please use System.Net.IPNetwork instead. For more information, visit https://aka.ms/aspnet/deprecate/005.' (https://aka.ms/aspnet/deprecate/005)

İkincisi, eski hosting modelinin kendisi. .NET Core 3.1 ve .NET 5 döneminden kalma bir Program.cs'i .NET 10 ile derledim:

// .NET Core 3.1 / .NET 5 dönemi hosting, .NET 10 ile derleniyor
using Microsoft.AspNetCore;

WebHost.CreateDefaultBuilder(args)
    .UseUrls("http://localhost:5000")
    .UseStartup<Startup>()
    .Build()
    .Run();

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints => endpoints.MapGet("/api/books/1", () => "Dune"));
    }
}
Program.cs(4,1): warning ASPDEPR008: 'WebHost' is obsolete: 'WebHost is obsolete. Use HostBuilder or WebApplicationBuilder instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)
Program.cs(4,1): warning ASPDEPR008: 'IWebHostBuilder.Build()' is obsolete: 'IWebHost is obsolete. Use IHost instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)

$ curl -s http://localhost:5000/api/books/1
Dune

Kod derleniyor ve çalışıyor; .NET 10, WebHostBuilder, IWebHost ve WebHost'u obsolete işaretledi (doğrudan new WebHostBuilder() yazan kod ASPDEPR004 alıyor). using Microsoft.AspNetCore; satırı olmadan derleyici WebHost'u erişilemeyen başka bir tipe bağlayıp CS0122 hatası verdi; eski projeleri taşırken bu kafa karıştırabiliyor. Bu kodu WebApplication.CreateBuilder'a taşımanın adımları ASP.NET Core Web API serisinin "Eski .NET Core 3.1 ve .NET 5 Projelerini Okumak: Startup.cs'ten .NET 6'ya Geçiş" yazısında. Hosting açısından önemli olan, UseUrls'ün de ikinci yazıdaki öncelik sırasına tabi olması ve container'da localhost'a bağlanmanın aynı tuzağa düşürmesi.

ANCM ve IIS Tarafında Değişenler

IIS (Internet Information Services) tarafındaki değişiklikler daha sessiz. In-process hosting ASP.NET Core 3.0'dan beri varsayılan. Shadow copy .NET 6'da experimentalEnableShadowCopy handler ayarıyla deneysel olarak geldi, .NET 7'den beri enableShadowCopy adıyla destekleniyor; .NET 6 döneminden kalma bir web.config'te deneysel adı görüyorsan onuncu yazıdaki gibi güncelle. ANCM (ASP.NET Core Module), desteklenen .NET sürümleriyle ileri ve geri uyumlu; tek bir güncel Hosting Bundle eski ve yeni uygulamaları birlikte taşıyor.

Bir de servis sürümüyle gelen davranış değişikliği var: ASP.NET Core 8.0.17 ve 9.0.6'dan beri forwarded headers middleware, bilinmeyen proxy'lerden gelen X-Forwarded-Proto, X-Forwarded-Host ve X-Forwarded-Prefix'i de yok sayıyor. Major sürüm değişmeden, bir apt upgrade ya da image güncellemesiyle gelen bu değişiklik, güvenilir proxy listesi eksik olan uygulamalarda HTTPS (HTTP Secure) redirection döngüsü olarak ortaya çıktı. Üçüncü yazıdaki "Unknown proxy" log'u burada da ilk bakılacak yer.

Eski Tutorial'ları Okurken

Hosting rehberleri uzun yaşıyor; arama motorunda ilk çıkan sonuç çoğu zaman .NET Core 2.x ya da 3.1 dönemine ait. Bu seride gördüğümüz eski kalıntıların listesi:

Eski tutorial'daBugünYazı
docker run -p 8000:80-p 8000:8080; image varsayılanı 8080Bu yazı
ENV ASPNETCORE_URLS=http://+:80Gereksiz; ASPNETCORE_HTTP_PORTS zaten 80802 ve bu yazı
Startup.cs, WebHost.CreateDefaultBuilder, UseUrlsWebApplication.CreateBuilder; eski API'ler obsoleteBu yazı, Web API 32
KnownNetworks.Add(new IPNetwork(...))KnownIPNetworks.Add(System.Net.IPNetwork.Parse(...))3
Nginx'te proxy_http_version 1.1 ve keepalive zorunlu1.29.7'den beri varsayılan; upstream block'u yeter5
listen 443 ssl http2;listen 443 ssl; ve http2 on;7
KillSignal=SIGINTKillSignal=SIGTERM (systemd varsayılanı)8
ExecStart=/usr/local/bin/dotnet ...Ubuntu paketinde /usr/bin/dotnet8
"Önce Microsoft paket deposunu ekle"Ubuntu 24.04 ve sonrasında .NET dağıtımın deposunda8
experimentalEnableShadowCopyenableShadowCopy (.NET 7+)10

Bir rehberin yaşını anlamanın en hızlı yolu Startup.cs, port 80 ve ASPNETCORE_URLS'e bakmak. Bunlardan biri varsa rehberin geri kalanındaki port, kullanıcı ve sinyal ayarlarını da bu tabloyla karşılaştır.

.NET 11 ile Gelenler

.NET 11, bu yazının yayınından iki gün sonra GA oluyor; Microsoft'un .NET 11 için yayımladığı breaking change listesinde hosting'e dokunan üç madde var. Kestrel, HTTP (Hypertext Transfer Protocol) 2 ve 3 isteklerinde RFC (Request for Comments) 9113 ve 9114'ün bağlantıya özgü saydığı header'ları (Connection, Keep-Alive, Transfer-Encoding, Upgrade ve diğerleri) artık reddediyor ve HTTP/1.1'de hem Content-Length hem Transfer-Encoding içeren bir istekten sonra bağlantıyı kapatıyor; doküman ikincisinin önceki desteklenen sürümlere de servis güncellemesiyle geldiğini söylüyor. Kestrel'in HTTP/3'te ilk isteği kontrol stream'ini beklemeden işlemesi ilk istek gecikmesini azaltıyor. Response compression middleware'i, sıkıştırmadığı yanıtlara da Vary: Accept-Encoding ekliyor; önünde Nginx ya da CDN (Content Delivery Network) cache'i olan uygulamalarda cache anahtarlarını etkileyebilir. .NET 11 GA olduktan sonra Bookstore'u yükseltip bu maddeleri bu serideki deneylerle tekrar ölçmek ayrı bir yazının konusu.

Üretimde Dikkat

Major sürüm yükseltmesini hosting açısından da test et. Uygulama kodu derleniyor ve testler geçiyor olabilir; ama image tabanının Debian'dan Ubuntu'ya geçmesi, varsayılan portun değişmesi ya da bir servis güncellemesindeki forwarded headers sıkılaştırması ancak gerçek proxy ve gerçek container ile görünüyor. On birinci yazıdaki kontrol script'ini yükseltme pipeline'ının parçası yap.

Obsolete uyarılarını görmezden gelme. ASPDEPR005 ve ASPDEPR008 bugün uyarı, ama Microsoft obsolete API'leri sonraki sürümlerde kaldırabiliyor; .NET 11'in breaking change listesinde daha önce obsolete işaretlenmiş API'lerin kaldırıldığı maddeler var. TreatWarningsAsErrors açık bir projede bu uyarılar yükseltmeyi bir gün durduracak; erken temizle.

Sık Düşülen Tuzaklar

Eski docker run -p ...:80 komutunu yeni image'la kullanmak. Container ayakta, log temiz, client "connection reset" alıyor.

.NET 8'in container'ları varsayılan olarak non-root çalıştırdığını sanmak. Varsayılan hâlâ root; USER $APP_UID senin işin.

Docker'da çalışan root olmayan 80 portunun her yerde çalışacağını varsaymak. Docker'ın sysctl varsayılanı başka runtime'larda yok; 8080'de kal.

Dockerfile'da eski ASPNETCORE_URLS satırını bırakmak. Image'ın 8080'ini eziyor ve iki farklı port bilgisi aynı image'da yaşıyor.

Servis sürüm güncellemelerini davranış değişikliği içermez sanmak. 8.0.17'deki forwarded headers değişikliği major yükseltme olmadan geldi.

Küçük Bir Deneme

★☆☆ mcr.microsoft.com/dotnet/aspnet:10.0 tabanlı bir Dockerfile yaz, USER $APP_UID ekle ve uygulamayı çalıştır. Data Protection key'leri nereye yazılıyor? Container yeniden oluşturulduğunda ne oluyor ve üçüncü yazıdaki çözümü container'da nasıl uygularsın?

★★☆ Eski WebHost.CreateDefaultBuilder örneğini container'da ASPNETCORE_URLS vermeden çalıştır ve hangi adresi dinlediğine bak. Sonra aynı uygulamayı WebApplication.CreateBuilder'a taşı ve image'ın ASPNETCORE_HTTP_PORTS'unu kullandığını doğrula.

★★★ .NET 11 yayımlandığında Bookstore.Api'yi net11.0'a yükselt ve bu serideki deneylerden üçünü tekrar çalıştır: ikinci yazının port önceliği, beşinci yazının upstream keepalive sayımı ve yedinci yazının HTTP/3 isteği. Hangi çıktılar değişti?

Seri burada bitiyor. Bookstore artık bir Linux VM'de (virtual machine) Nginx arkasında, systemd altında iki Kestrel instance'ıyla çalışıyor; Angular aynı origin'den geliyor; TLS (Transport Layer Security), header'lar, limit'ler ve health check yerinde ve hepsi bir kontrol script'iyle doğrulanıyor. Sıradaki adımlar başka serilerin konusu: certificate'ların yaşam döngüsü SSL (Secure Sockets Layer)/TLS Certificate'ları ve HTTPS serisinde, kesintisiz deploy Deployment Stratejileri'nde, aynı uygulamanın container'a taşınması da Docker ve Kubernetes serilerinde.