Mert Özen Her Satırda Daha İleri
DevOps Docker — #08

ASP.NET Core'u Container'a Almak: .NET 10 Image'ları, Chiseled, Non-root, Port 8080 ve Globalization

Mert Özen 14 Kas 2026 13 dk 9 görüntülenme
ASP.NET Core'u Container'a Almak: .NET 10 Image'ları, Chiseled, Non-root, Port 8080 ve Globalization

ASP.NET Core image'ları port 8080'i dinler ve APP_UID ile root olmayan app kullanıcısını taşır; chiseled image'da ise ICU ve tzdata olmadığı için Türkçe kültür ve Europe/Istanbul saat dilimi isteyen kod 500 döner. Bookstore.Api'yi container'a alıp image ailelerini karşılaştırıyor, bu hatayı chiseled-extra ile çözüyor, environment variable ile configuration, shell'siz health check ve memory limit'in GC'ye etkisini gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3, .NET SDK 10.0.404, ASP.NET Core Runtime 10.0.15 image'ları (mcr.microsoft.com/dotnet/aspnet:10.0-*) ve Microsoft.AspNetCore.OpenApi 10.0.15 ile denenmiştir. Boyutlar linux/amd64 image'larından alınmıştır.

Önce Tahmin Et

Bookstore.Api kitapları Türkçe kurallarla sıralıyor (tr-TR) ve mağazanın saatini Europe/Istanbul saat diliminde gösteriyor. Geliştirici makinesinde her şey doğru. Aynı kodu mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled image'ıyla container'a aldın. GET /books ne döner: (a) aynı sonuç, (b) "Çalıkuşu" ve "İnce Memed" listenin sonuna düşmüş bir sıralama, (c) 500?

Bookstore.Api ve İlk Dockerfile

Bu yazıda sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) asıl uygulamasını, ASP.NET Core Web API'si Bookstore.Api'yi container'a alıyoruz. Veritabanı bağlantısını volume ve network yazılarına (11 ve 12) bırakıp küçük başladık: katalog şimdilik bellekte, iki endpoint ve bir health check var. Container'a özgü davranışı görmek için gereken kısım şu:

public sealed class BookCatalog(IOptions<StoreOptions> options)
{
    private static readonly Book[] Books =
    [
        new("9780441172719", "Dune", "Frank Herbert", 420.00m),
        new("9789750719387", "İnce Memed", "Yaşar Kemal", 310.00m),
        new("9789750738609", "Çalıkuşu", "Reşat Nuri Güntekin", 240.00m),
        new("9789754700114", "Saatleri Ayarlama Enstitüsü", "Ahmet Hamdi Tanpınar", 280.00m),
        new("9780141439587", "Emma", "Jane Austen", 180.00m),
        new("9789750722752", "Kürk Mantolu Madonna", "Sabahattin Ali", 150.00m),
    ];

    public IReadOnlyList<BookView> SortedByTitle()
    {
        CultureInfo culture = CultureInfo.GetCultureInfo(options.Value.Culture);
        StringComparer comparer = StringComparer.Create(culture, ignoreCase: false);

        return [.. Books
            .OrderBy(b => b.Title, comparer)
            .Select(b => new BookView(b.Isbn, b.Title, b.Title.ToUpper(culture), b.PriceTry))];
    }
}
app.MapHealthChecks("/health");
app.MapGet("/books", (BookCatalog catalog) => catalog.SortedByTitle());
app.MapGet("/store/clock", (IOptions<StoreOptions> options) =>
{
    TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(options.Value.TimeZone);
    DateTimeOffset local = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, zone);
    return new { zone = zone.Id, localTime = local.ToString("yyyy-MM-dd HH:mm zzz", CultureInfo.InvariantCulture) };
});

app.Lifetime.ApplicationStarted.Register(() =>
    app.Logger.LogInformation("Environment: {Environment}, GC memory limit: {LimitMb} MB, processors: {Cpus}",
        app.Environment.EnvironmentName,
        GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 1024 / 1024,
        Environment.ProcessorCount));

ShelfLabel, rafa basılan büyük harfli etiket: "Saatleri Ayarlama Enstitüsü" Türkçe kurallarla "SAATLERİ AYARLAMA ENSTİTÜSÜ" olmalı. Culture ve TimeZone appsettings.json'daki Store bölümünden geliyor. Dockerfile 7. yazıdaki yapının API'ye uyarlanmış hâli:

# syntax=docker/dockerfile:1
ARG DOTNET_VERSION=10.0

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
ARG TARGETARCH
ARG BUILD_CONFIGURATION=Release
ARG APP_VERSION=1.0.0
WORKDIR /src
COPY global.json ./
COPY src/Bookstore.Api/Bookstore.Api.csproj src/Bookstore.Api/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
    dotnet restore src/Bookstore.Api/Bookstore.Api.csproj -a $TARGETARCH
COPY src/Bookstore.Api/ src/Bookstore.Api/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
    dotnet publish src/Bookstore.Api/Bookstore.Api.csproj -c $BUILD_CONFIGURATION -a $TARGETARCH \
        --no-restore -o /app -p:Version=$APP_VERSION

FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-noble-chiseled AS final
ARG APP_VERSION=1.0.0
WORKDIR /app
COPY --from=build /app ./
LABEL org.opencontainers.image.title="Bookstore.Api" \
      org.opencontainers.image.version=$APP_VERSION
EXPOSE 8080
USER $APP_UID
ENTRYPOINT ["dotnet", "Bookstore.Api.dll"]

7. yazıdaki Bookstore.Tools Dockerfile'ından bir fark var: restore'u -a $TARGETARCH ile yapıyoruz, böylece hedef mimarinin restore'u da ayrı layer'da kalıyor ve publish --no-restore ile çalışabiliyor. Yalnız Api'nin csproj'unu ve klasörünü kopyalıyoruz; Tools'ta değişen bir dosya Api'nin cache'ini kırmıyor. Solution'a Api'yi eklemenin Tools Dockerfile'ında yarattığı bir sorunu Tuzaklar bölümünde göreceğiz.

6. yazıda başladığımız docker-bake.hcl'e bir api target'ı ekledik ve default ile ci gruplarına koyduk. lint target'ı artık bir matrix: aynı tanımdan her uygulama için ayrı bir check target'ı (lint-tools, lint-api) üretiliyor:

target "api" {
  context    = "."
  dockerfile = "src/Bookstore.Api/Dockerfile"
  target     = "final"
  args = {
    APP_VERSION = TAG
  }
  tags = ["dockerseries-bookstore-api:${TAG}"]
}

target "lint" {
  name     = "lint-${app}"
  matrix   = { app = ["tools", "api"] }
  inherits = [app]
  call     = "check"
}
docker buildx bake ci
#37 naming to docker.io/library/dockerseries-bookstore-tools:1.0.0 done
#41 naming to docker.io/library/dockerseries-bookstore-api:1.0.0 done
lint-api
Check complete, no warnings found.
lint-tools
Check complete, no warnings found.

Image Ailesini Seçmek

aspnet image'ı ASP.NET Core runtime'ını taşır ve birkaç varyantı var. .NET 10 ile 10.0 gibi işletim sistemi belirtmeyen tag'ler Debian yerine Ubuntu 24.04'ü ("Noble") gösteriyor ve Debian image'ları artık yayınlanmıyor. Microsoft'un gerekçesi destek süreleri: Ubuntu sürümleri bir .NET sürümünden daha uzun destekleniyor. Varyantların linux/amd64 image config'ine ve boyutuna baktık:

for t in 10.0 10.0-alpine 10.0-azurelinux3.0-distroless 10.0-noble-chiseled 10.0-noble-chiseled-extra 10.0-noble-chiseled-composite; do
    docker image inspect --platform linux/amd64 "mcr.microsoft.com/dotnet/aspnet:$t" \
        --format "$t User={{.Config.User}} Env={{json .Config.Env}}"
done
10.0 User= Env=["PATH=...","APP_UID=1654","ASPNETCORE_HTTP_PORTS=8080","DOTNET_RUNNING_IN_CONTAINER=true","DOTNET_VERSION=10.0.15","ASPNET_VERSION=10.0.15"]
10.0-noble-chiseled User=1654 Env=["PATH=...","APP_UID=1654","ASPNETCORE_HTTP_PORTS=8080","DOTNET_RUNNING_IN_CONTAINER=true","DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true","DOTNET_VERSION=10.0.15","ASPNET_VERSION=10.0.15"]
10.0-noble-chiseled-extra User=1654 Env=["PATH=...","APP_UID=1654","ASPNETCORE_HTTP_PORTS=8080","DOTNET_RUNNING_IN_CONTAINER=true","DOTNET_VERSION=10.0.15","ASPNET_VERSION=10.0.15"]

(PATH değerini kısalttım, üç satırı çıkardım; hepsi tabloda.) Globalization sütunu image'da ICU (International Components for Unicode; dile özel sıralama ve büyük-küçük harf kurallarını sağlayan kütüphane) ve tzdata (saat dilimi verisi) olup olmadığını gösteriyor; shell olup olmadığını docker run --entrypoint sh ile denedik:

TagKullanıcıGlobalizationShellDISK USAGE / CONTENT SIZE
10.0 (Ubuntu 24.04)rootICU ve tzdata varvar342MB / 97.7MB
10.0-alpinerootinvariantvar178MB / 55.7MB
10.0-azurelinux3.0-distroless1654invariantyok209MB / 63.8MB
10.0-noble-chiseled1654invariantyok182MB / 56.7MB
10.0-noble-chiseled-extra1654ICU ve tzdata varyok239MB / 72.6MB
10.0-noble-chiseled-composite1654invariantyok181MB / 56.1MB

Seçim için kısa bir yol: shell'e ve paket yöneticisine ihtiyacın yoksa (production'da olmamalı) chiseled; globalization gerekiyorsa chiseled-extra; build sırasında apt-get ile native bir paket kurman gerekiyorsa Ubuntu tabanlı 10.0. Alpine, musl libc kullandığı için native bağımlılıklarda sürpriz çıkarabilir; Azure Linux distroless, Azure ekosisteminde standartlaşmış ekipler için. Composite varyantta framework assembly'leri tek bir ReadyToRun (önceden makine koduna derlenmiş) binary'de birleştirilmiştir; Microsoft'un dokümanına göre JIT (Just-In-Time; çalışma anında makine koduna derleme) işi azaldığı için başlangıç hızlanır. Bookstore.Api'yi chiseled ile başlattık.

Port 8080 ve Root Olmayan Kullanıcı

Image config'indeki iki ortam değişkeni bu bölümün özeti. ASPNETCORE_HTTP_PORTS=8080, Kestrel'in hangi port'u dinleyeceğini söyler; .NET 8'den beri bütün resmi image'larda 80 yerine 8080. APP_UID=1654, image'larda hazır gelen app kullanıcısının uid'si. Chiseled ve distroless image'lar zaten bu kullanıcıyla başlar (User=1654); Ubuntu ve Alpine image'larının User alanı boş, yani USER satırı yazmazsan root olarak çalışırsın. Dockerfile'daki USER $APP_UID bu yüzden her varyantta aynı sonucu verir ve image değiştiğinde kullanıcı sessizce root'a dönmez. Container'ı başlatıyoruz:

docker build -f src/Bookstore.Api/Dockerfile -t dockerseries-bookstore-api:1.0.0 .
docker run -d --name dockerseries-api -p 8080:8080 dockerseries-bookstore-api:1.0.0
docker logs dockerseries-api
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

Kestrel bütün arayüzlerde ([::]) 8080'i dinliyor; container içinde localhost'a bind olsaydı host'tan ulaşamazdın (bunu 12. yazıda göreceğiz). -p 8080:8080 host'un 8080'ini container'ın 8080'ine bağlıyor; Dockerfile'daki EXPOSE 8080 ise 5. yazıda gördüğümüz gibi yalnız dokümantasyon. Port'u değiştirmek istersen -e ASPNETCORE_HTTP_PORTS=5000 yeterli; ASPNETCORE_URLS da hâlâ çalışır ve verilirse ASPNETCORE_HTTP_PORTS'u ezer; denediğimizde Kestrel bunu Overriding HTTP_PORTS '8080' and HTTPS_PORTS '' uyarısıyla log'ladı. Eski kurulumlardaki port 80'den 8080'e geçişi 15. yazıda ayrıca ele alacağız.

Environment Variable ile Configuration

ASP.NET Core'un configuration sistemi ortam değişkenlerini okur ve __ (iki alt çizgi) bölüm ayıracıdır: Store__TimeZone, appsettings.json'daki Store:TimeZone'u ezer. Provider sırası ve options pattern ASP.NET Core Web API serisinin "ASP.NET Core'da Configuration ve Options Pattern: Provider Sırası, User Secrets ve Validation" yazısında anlatıldı; container'da önemli olan, aynı image'ın her ortamda yalnız ortam değişkenleriyle farklı davranabilmesi. ASPNETCORE_ENVIRONMENT verilmezse ortam Production'dır ve Bookstore.Api OpenAPI dokümanını yalnız Development'ta açıyor:

curl -s -o /dev/null -w "openapi: %{http_code}\n" localhost:8080/openapi/v1.json
docker rm -f dockerseries-api
docker run -d --name dockerseries-api -p 8080:8080 \
    -e ASPNETCORE_ENVIRONMENT=Development -e Store__TimeZone=UTC dockerseries-bookstore-api:1.0.0
curl -s -o /dev/null -w "openapi: %{http_code}\n" localhost:8080/openapi/v1.json
curl -s localhost:8080/store/clock
openapi: 404
openapi: 200
{"zone":"UTC","localTime":"2026-11-14 11:30 +00:00"}

Veritabanı bağlantısı da aynı yoldan gelecek: 12. yazıda SQL Server container'ına -e ConnectionStrings__Default="Server=bookstore-db;..." ile bağlanacağız. Ortam değişkenleri docker inspect ile okunabildiği için şifre içeren değerleri production'da bir secret store'dan vermek gerekir; bunu 13. yazıda ve Configuration ve Secret Management serisinde konuşacağız.

Chiseled'da Türkçe ve Saat Dilimi: Bir Bookstore Bug'ı

Şimdi ilk sorunun cevabı. Chiseled image'la build edilmiş container'a istek attık:

curl -s -i localhost:8080/books | head -1
curl -s -i localhost:8080/store/clock | head -1
docker logs dockerseries-api 2>&1 | grep -E "fail|Exception"
HTTP/1.1 500 Internal Server Error
HTTP/1.1 500 Internal Server Error
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      System.Globalization.CultureNotFoundException: Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information. (Parameter 'name')
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      System.TimeZoneNotFoundException: The time zone ID 'Europe/Istanbul' was not found on the local computer.
       ---> System.IO.DirectoryNotFoundException: Could not find a part of the path '/usr/share/zoneinfo/Europe/Istanbul'.

Cevap (c). Chiseled image'da ICU yok, image DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true ile geliyor ve invariant modda .NET tr-TR gibi bir kültür istendiğinde exception fırlatıyor. Saat dilimi verisi (tzdata, /usr/share/zoneinfo) de yok. Geliştirici makinesinde ikisi de işletim sisteminde hazır olduğu için hata ancak container'da çıkıyor.

Bu noktada internette en sık önerilen "çözüm" exception'ı susturmak: DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false. Denedik:

docker run -d --name dockerseries-api -p 8080:8080 \
    -e DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false dockerseries-bookstore-api:chiseled
curl -s localhost:8080/books | jq -r '.[] | "\(.title) | \(.shelfLabel)"'
Dune | DUNE
Emma | EMMA
Kürk Mantolu Madonna | KÜRK MANTOLU MADONNA
Saatleri Ayarlama Enstitüsü | SAATLERI AYARLAMA ENSTITÜSÜ
Çalıkuşu | ÇALıKUŞU
İnce Memed | İNCE MEMED

500 gitti, bug kaldı ve artık sessiz. Sıralama ordinal: "Çalıkuşu" ve "İnce Memed", Unicode kodları Latin harflerinden büyük olduğu için listenin sonunda. Büyük harf dönüşümü Türkçe kuralları bilmiyor: "SAATLERI" ve "ENSTITÜSÜ" noktasız I ile, "ÇALıKUŞU"nun ortasındaki ı hiç dönüşmemiş. Rafa bu etiketler basılırdı. Asıl çözüm uygulamanın ihtiyacını taşıyan image'ı seçmek; Dockerfile'da tek satır:

FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-noble-chiseled-extra AS final
curl -s localhost:8080/books | jq -r '.[] | "\(.title) | \(.shelfLabel)"'
curl -s localhost:8080/store/clock
Çalıkuşu | ÇALIKUŞU
Dune | DUNE
Emma | EMMA
İnce Memed | İNCE MEMED
Kürk Mantolu Madonna | KÜRK MANTOLU MADONNA
Saatleri Ayarlama Enstitüsü | SAATLERİ AYARLAMA ENSTİTÜSÜ
{"zone":"Europe/Istanbul","localTime":"2026-11-14 14:29 +03:00"}

Ç, C'den sonra; İ, I'dan sonra geliyor, büyük harfler doğru ve saat +03:00. Bedeli boyut: Bookstore.Api image'ı chiseled ile 183 MB / 57 MB iken chiseled-extra ile 240 MB / 73 MB. Uygulama gerçekten kültüre duyarlı iş yapmıyorsa tersini yapmak da mümkün: csproj'a <InvariantGlobalization>true</InvariantGlobalization> yazarsan geliştirici makinesi de invariant modda çalışır ve aynı hata container'a gitmeden, lokalde görünür. Önemli olan, iki ortamın aynı modda çalışması.

Shell'siz Image'da Health Check

Dockerfile'daki HEALTHCHECK instruction'ı Docker'a container'ın sağlığını nasıl ölçeceğini söyler. Klasik örnek HEALTHCHECK CMD curl -f http://localhost:8080/health, ama chiseled image'da ne curl var ne de shell form'u çalıştıracak /bin/sh. Container'ın içinde zaten çalışabilen tek program dotnet; uygulamanın kendisine küçük bir probe modu ekledik:

namespace Bookstore.Api;

// "dotnet Bookstore.Api.dll healthcheck": shell'i ve curl'ü olmayan image'lar için probe
public static class HealthProbe
{
    public static async Task<int> RunAsync()
    {
        string port = (Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS") ?? "8080").Split(';')[0];
        using HttpClient http = new() { Timeout = TimeSpan.FromSeconds(3) };
        try
        {
            using HttpResponseMessage response = await http.GetAsync($"http://localhost:{port}/health");
            return response.IsSuccessStatusCode ? 0 : 1;
        }
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
        {
            return 1;
        }
    }
}
if (args is ["healthcheck"])
{
    return await HealthProbe.RunAsync();
}
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD ["dotnet", "Bookstore.Api.dll", "healthcheck"]

Exec form zorunlu, çünkü shell yok. Probe /health'e, ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Health Check: Liveness, Readiness ve Veritabanı Kontrolü" yazısında kurduğumuz endpoint'e istek atıyor:

docker ps --format "table {{.Names}}\t{{.Status}}"
docker inspect dockerseries-api --format "{{json .State.Health}}" | jq ".Status, .Log[-1]"
NAMES              STATUS
dockerseries-api   Up 3 seconds (health: starting)
NAMES              STATUS
dockerseries-api   Up 15 seconds (healthy)
"healthy"
{
  "Start": "2026-11-14T11:30:23.736089837Z",
  "End": "2026-11-14T11:30:24.326524087Z",
  "ExitCode": 0,
  "Output": ""
}

Bu yöntemin bir maliyeti var: her 30 saniyede bir yeni bir .NET process'i başlıyor ve kapanıyor (log'daki Start ve End arası yaklaşık 0,6 saniye); bu süre ve bellek container'ın limitinden yeniyor. Docker'ın health durumu docker ps'te görünür ve Docker Compose'daki depends_on: condition: service_healthy gibi özellikler onu kullanır. Kubernetes ise Dockerfile'daki HEALTHCHECK'i hiç okumaz; kendi probe'larıyla doğrudan HTTP isteği atar ve container'ın içinde process başlatmaz. Kubernetes serisinin "Kubernetes Health Probe'ları: Startup, Liveness, Readiness ve ASP.NET Core Health Check'leri" yazısı orayı anlatıyor. Image yalnız Kubernetes'te çalışacaksa HEALTHCHECK satırını yazmamak da makul bir tercih.

Memory Limit ve GC

Container'a --memory ve --cpus ile sınır verince .NET bunu cgroup'tan okur ve GC'yi (Garbage Collector; kullanılmayan belleği geri toplayan bileşen) ona göre ayarlar. Bookstore.Api başlarken GC'nin gördüğü belleği ve işlemci sayısını log'luyor:

docker run -d --name dockerseries-api -p 8080:8080 --memory 256m --cpus 1 dockerseries-bookstore-api:1.0.0
docker logs dockerseries-api 2>&1 | tail -2
info: Bookstore.Api[0]
      Environment: Production, GC memory limit: 192 MB, processors: 1

256 MB'lık limitin %75'i, 192 MB, GC heap'inin üst sınırı oldu; kalan pay native bellek, thread stack'leri ve runtime'ın kendisi için. --cpus 1 ile Environment.ProcessorCount 1 döndü, bu da GC modunu ve thread pool'un başlangıç boyutunu etkiler. Limit vermezsen .NET makinenin (Docker Desktop'ta WSL (Windows Subsystem for Linux; Windows'ta Linux çalıştıran alt sistem) 2'nin VM'inin) bütün belleğini görür ve container'ı başka işlerle paylaşılan bir host'ta beklenenden çok büyüyebilir. Heap sınırını DOTNET_GCHeapHardLimitPercent gibi ayarlarla değiştirmek ve OOMKilled teşhisi Kubernetes serisinin "Resource Request ve Limit'ler: QoS, OOMKilled, CPU Throttling ve Container'da .NET GC" yazısında; 137 ile ölen bir container'ı 14. yazıda inceleyeceğiz.

Üretimde Dikkat

TLS'i (Transport Layer Security; HTTPS'in şifreleme katmanı) container'da değil önündeki reverse proxy'de ya da load balancer'da sonlandır. Image yalnız HTTP 8080 dinler; sertifika, yenileme ve HTTP/2–HTTP/3 ayarları proxy'nin işidir. Bu, sertifikanın image'a girmesini de engeller.

Base image tag'ini bilinçli sabitle. aspnet:10.0-noble-chiseled-extra her .NET patch'inde (ayda bir civarı) güncellenir ve güvenlik yamalarını getirir; 4. yazıdaki digest pin + otomatik güncelleme ile bu güncellemeyi bir pull request olarak görürsün.

Uygulamanın framework sürümünü runtime image'ıyla uyumlu tut. Image ASPNET_VERSION=10.0.15 taşıyor; uygulamayı daha yeni bir patch'e özgü bir pakete bağlarsan runtime eski kalabilir. docker run --rm --entrypoint dotnet <image> --list-runtimes image'daki runtime'ları gösterir.

Image'ı root olmayan kullanıcıyla build ettin ama container'ı çalıştıran ortam bunu ezebilir (docker run --user 0, Kubernetes securityContext). Bu ayarları ve --read-only, --cap-drop gibi ek katmanları 13. yazıda konuşacağız.

Sık Düşülen Tuzaklar

appsettings.Development.json'u image'a koymak. dotnet publish bu dosyayı varsayılan olarak çıktıya kopyalar. Container'ın içine baktık:

docker create --name dockerseries-inspect dockerseries-bookstore-api:1.0.0
docker export dockerseries-inspect | tar -tf - | grep "^app/appsettings"
docker rm dockerseries-inspect
app/appsettings.Development.json
app/appsettings.json

Biri container'ı ASPNETCORE_ENVIRONMENT=Development ile başlatırsa geliştirme ayarları (ayrıntılı log seviyeleri, lokal bağlantı adresleri, bazen test şifreleri) devreye girer. csproj'a eklediğimiz satırla dosya publish çıktısına hiç girmiyor; aynı komut artık yalnız app/appsettings.json listeliyor:

<ItemGroup>
  <!-- Geliştirme ayarları geliştiricinin makinesinde kalır, image'a girmez -->
  <Content Update="appsettings.Development.json" CopyToPublishDirectory="Never" />
</ItemGroup>

Development certificate'ı image'a koymak. dotnet dev-certs ile üretilen sertifikayı ve şifresini image'a kopyalayıp Kestrel'de HTTPS açmak lokal bir denemeyi kolaylaştırır ama image'ı çeken herkese bir özel anahtar dağıtır. Production'da TLS proxy'de; lokal geliştirmede container HTTP ile çalışsın.

Solution dosyası üzerinden restore yapıp yalnız bazı projeleri kopyalamak. Api'yi Bookstore.slnx'e ekledikten sonra 7. yazıdaki Tools Dockerfile'ı build edilemez oldu:

error MSB3202: The project file "/src/src/Bookstore.Api/Bookstore.Api.csproj" was not found. [/src/Bookstore.slnx]

Tools Dockerfile'ı dotnet restore Bookstore.slnx diyor ama Api'nin csproj'unu kopyalamıyordu. Restore'u solution yerine test projesi üzerinden yapacak şekilde değiştirdik (test projesi Tools'u referans ettiği için ikisi birden restore ediliyor) ve solution'ı kopyalamayı bıraktık. Kural: Dockerfile'da restore ettiğin şey, kopyaladığın csproj'larla tutarlı olmalı.

Globalization hatasını PREDEFINED_CULTURES_ONLY=false ile susturmak. Yukarıda gördüğümüz gibi exception gider, yanlış sıralama ve yanlış büyük harfler kalır. Ya kültür desteği olan image'ı seç ya da uygulamayı her ortamda bilerek invariant çalıştır.

Küçük Bir Deneme

★☆☆ Final stage'i aspnet:10.0 (Ubuntu) yap ve USER $APP_UID satırını sil. docker exec dockerseries-api id ne gösteriyor? Satırı geri koyunca ne değişiyor?

★★☆ csproj'a <InvariantGlobalization>true</InvariantGlobalization> ekleyip uygulamayı lokalde dotnet run ile çalıştır. /books artık lokalde de 500 dönüyor mu? Image'ı chiseled'a geri alırsan container'da davranış lokalle aynı mı?

★★★ Container'ı --memory 128m ile başlat ve /books'a bir yük aracıyla (ör. hey ya da bombardier) yüzlerce istek gönder. docker stats'ta bellek nerede duruyor, container ölüyor mu? DOTNET_GCHeapHardLimitPercent=50 ile tekrarla.

Bookstore.Api'nin Dockerfile'ı hazır. Bir sonraki yazıda aynı API'yi hiç Dockerfile yazmadan, .NET SDK'nın dotnet publish /t:PublishContainer desteğiyle image'a çeviriyoruz ve iki yolu boyut, layer ve build süresi açısından karşılaştırıyoruz.