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

Container Yaşam Döngüsü: docker run, ps, logs, exec, stop ve Exit Code'lar

Mert Özen 14 Kas 2026 14 dk 13 görüntülenme
Container Yaşam Döngüsü: docker run, ps, logs, exec, stop ve Exit Code'lar

docker stop container'ın PID 1'ine SIGTERM (ya da image'ın STOPSIGNAL'ını) gönderir, 10 saniye bekler ve SIGKILL atar; uygulama shell'in arkasında başlatılmışsa sinyal hiç ulaşmaz ve container 137 ile ölür. Container durumlarını, logs ve exec'i, 0/1/137/139/143 exit code'larını, .NET 10'da ProcessExit'in SIGTERM'de artık çalışmamasını, Generic Host ile graceful shutdown'ı ve restart policy'lerini gerçek çıktılarla inceliyoruz.

Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3, .NET 10 (runtime 10.0.15, Microsoft.Extensions.Hosting 10.0.15), karşılaştırma için .NET 9 (runtime 9.0.23) ve nginx 1.30 ile denenmiştir.

Önce Tahmin Et

Bookstore'un katalog import aracı Bookstore.Tools bir container'da çalışıyor ve container şu komutla başlatılmış: sh -c 'dotnet Bookstore.Tools.dll; echo import done'. Deploy sırasında docker stop çağrılıyor. Komut kaç saniye sürer ve container hangi exit code ile kapanır? İkinci soru: aynı aracın sinyallerle hiç ilgilenmeyen basit bir .NET 10 console uygulaması olduğunu, kapanırken iş yapsın diye AppDomain.ProcessExit'e bir handler bağlandığını düşün. docker stop'ta o handler çalışır mı? .NET 9'da çalışıyordu.

docker run: İlk Container ve Bayraklar

Önceki yazılarda docker run'ı birkaç kez kullandık; şimdi neyi tek seferde yaptığını ayıralım. docker run aslında iki adımdır: docker create container'ı (dosya sistemi katmanı, ayarlar, network bağlantısı) hazırlar, docker start process'i başlatır. İkisini ayrı çalıştırınca ara durumu görürsün:

docker create --name dockerseries-web -p 8080:80 nginx:1.30-alpine
docker ps -a --format "table {{.Names}}\t{{.Status}}"
docker start dockerseries-web
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES              STATUS
dockerseries-web   Created
NAMES              STATUS                  PORTS
dockerseries-web   Up Less than a second   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Günlük kullanımda bayrakların çoğunu docker run'a verirsin. -d container'ı arka planda başlatır ve ID'sini yazar; vermezsen terminal container'ın çıktısına bağlı kalır. --name sabit bir ad verir; vermezsen Docker rastgele bir ad üretir ve aynı adla ikinci bir container açamazsın. -p 8080:80 host'un 8080 port'unu container'ın 80 port'una yönlendirir (ayrıntıları 12. yazıda). -e ConnectionStrings__Default=... ortam değişkeni verir; .NET'in configuration sistemi bunları okur. --rm container durunca onu siler. -it ise iki bayraktır: -i standart girişi açık tutar, -t bir terminal ayırır; docker run -it --rm alpine:3 sh sana container içinde etkileşimli bir shell verir ve exit dediğinde her şey silinir.

curl -s localhost:8080 | grep -o "<title>.*</title>"
docker logs dockerseries-web 2>&1 | grep -E "GET /"
<title>Welcome to nginx!</title>
172.18.0.1 - - [14/Nov/2026:10:34:10 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.23.0" "-"

Container'ın Durumları

Bir container'ın hayatı birkaç durum arasında geçer ve docker inspect'in .State alanı bunun kaydını tutar:

DurumNasıl girilirNe anlama gelir
createddocker createContainer hazır, process hiç başlamadı
runningdocker start, docker runAna process çalışıyor
pauseddocker pauseProcess'ler cgroup freezer ile donduruldu
restartingRestart policy devredeProcess bitti, Docker yeniden başlatıyor
exitedAna process bitti, stop, killDosya sistemi ve log'lar duruyor, start ile yeniden başlar
deadSilme işlemi yarıda kaldıNadir; genellikle elle silinir

Kritik nokta: container'ın ömrü ana process'in ömrüdür. Ana process biterse container exited olur; içinde başka process'ler çalışıyor olsa bile. exited bir container silinmediği sürece yazılabilir katmanını ve log'larını diskte tutar.

logs ve exec

docker logs container'ın ana process'inin standart çıktısını ve hata çıktısını gösterir. Uygulama dosyaya log yazıyorsa docker logs onları görmez; container'da log'u stdout'a yazmak bu yüzden kural. -f canlı izler, --tail 50 son satırlarla sınırlar, --since 10m zamana göre süzer, -t her satıra Docker'ın zaman damgasını ekler. Log'lar container durduktan sonra da okunabilir; bu, aşağıdaki exit code analizlerinde işimize yarayacak.

docker exec çalışan bir container'ın içinde ek bir process başlatır: docker exec dockerseries-web nginx -v tek komut çalıştırır, docker exec -it dockerseries-web sh etkileşimli bir shell açar. exec ile açılan process aynı namespace'leri ve cgroup'u paylaşır, ama container'ın ana process'i değildir: shell'den çıkman container'ı durdurmaz. exec yalnız running durumdaki container'da çalışır; çöken bir container'a girmek için önce log'lara ve exit code'a bakarsın.

stop ve kill: Sinyaller ve 10 Saniye

docker stop container'ın ana process'ine bir durdurma sinyali gönderir, bir süre bekler, process hâlâ yaşıyorsa SIGKILL ile öldürür. Docker'ın dokümanına göre sinyal image'daki STOPSIGNAL'dan gelir, tanımlı değilse SIGTERM'dür; bekleme süresi Linux container'larında 10, Windows container'larında 30 saniyedir. nginx image'ı farklı bir sinyal seçmiş:

docker inspect -f "StopSignal={{.Config.StopSignal}}" dockerseries-web
docker stop dockerseries-web
docker logs dockerseries-web 2>&1 | grep -E "SIGQUIT|gracefully" | head -3
docker inspect -f "Status={{.State.Status}} ExitCode={{.State.ExitCode}}" dockerseries-web
StopSignal=SIGQUIT
2026/11/14 10:33:49 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2026/11/14 10:33:49 [notice] 30#30: gracefully shutting down
2026/11/14 10:33:49 [notice] 31#31: gracefully shutting down
Status=exited ExitCode=0

nginx için SIGQUIT "açık bağlantıları bitir, sonra kapan" demek; SIGTERM ise hızlı kapanış. Image sahibi doğru sinyali STOPSIGNAL ile seçmiş, sen hiçbir şey yapmadan graceful kapanış alıyorsun. docker kill ise varsayılan olarak doğrudan SIGKILL gönderir:

docker start dockerseries-web
docker kill dockerseries-web
docker ps -a --format "table {{.Names}}\t{{.Status}}"
NAMES              STATUS
dockerseries-web   Exited (137) Less than a second ago

Bekleme süresini tek seferlik docker stop -t 30 ile, container başına docker run --stop-timeout 30 ile değiştirirsin; Engine 29.7'den beri daemon tarafında default-stop-timeout ayarı da var. Değiştirmen gereken durum genelde şudur: uygulamanın kapanması 10 saniyeden uzun sürüyorsa, ya uygulamayı hızlandırırsın ya da bekleme süresini uygulamanın kapanma süresinden biraz uzun tutarsın.

Exit Code'ları Okumak

Container'ın exit code'u ana process'inkidir. Process kendisi çıktıysa kendi verdiği kodu, bir sinyalle öldürüldüyse 128 + sinyal numarasını görürsün. Birkaç durumu bilerek üretelim:

docker run --name dockerseries-exit0 alpine:3 sh -c "exit 0"
docker run --name dockerseries-exit1 alpine:3 sh -c "echo connection failed >&2; exit 1"
docker run --name dockerseries-oom --memory 64m alpine:3 sh -c "tail /dev/zero"
docker ps -a --filter name=dockerseries- --format "table {{.Names}}\t{{.Status}}"
docker inspect -f "{{.Name}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}" dockerseries-exit1 dockerseries-oom
connection failed
NAMES                STATUS
dockerseries-oom     Exited (137) Less than a second ago
dockerseries-exit1   Exited (1) Less than a second ago
dockerseries-exit0   Exited (0) Less than a second ago
/dockerseries-exit1 ExitCode=1 OOMKilled=false
/dockerseries-oom ExitCode=137 OOMKilled=true

tail /dev/zero satır sonu bulamadığı için belleği doldurur; 64 MB sınırına ulaşınca kernel'in OOM (out of memory; bellek bitince process öldüren kernel mekanizması) killer'ı onu SIGKILL ile öldürdü. 137 tek başına "OOM" demek değildir: docker kill de, docker stop'un süre dolunca attığı SIGKILL de 137 üretir. Ayrımı OOMKilled alanı yapar. Sık göreceğin kodlar:

  • 0: process normal bitti.
  • 1: uygulama hatası; unhandled exception ya da uygulamanın kendi verdiği kod.
  • 137 (128 + 9, SIGKILL): docker kill, stop süresinin dolması ya da OOMKilled=true.
  • 139 (128 + 11, SIGSEGV): native kodda bellek hatası.
  • 143 (128 + 15, SIGTERM): process sinyalin varsayılan davranışıyla öldü.

139'u üretmeye çalışınca PID (process ID; kernel'in process'e verdiği numara) 1'in ilk sürprizi çıkıyor:

docker run --name dockerseries-segv alpine:3 sh -c "kill -SEGV \$\$"
docker run --init --name dockerseries-segv-init alpine:3 sh -c "kill -SEGV \$\$"
docker ps -a --filter name=dockerseries-segv --format "table {{.Names}}\t{{.Status}}"
NAMES                    STATUS
dockerseries-segv-init   Exited (139) Less than a second ago
dockerseries-segv        Exited (0) Less than a second ago

Shell kendine SIGSEGV gönderdi ama ilk container'da hiçbir şey olmadı ve shell 0 ile çıktı. Nedeni, kernel'in bir PID namespace'inin ilk process'ine (PID 1) özel davranması: PID 1'e gelen bir sinyal, process o sinyal için handler kaydetmemişse yok sayılır (yalnız SIGKILL ve SIGSTOP namespace dışından gelince istisna). --init verilen ikinci container'da PID 1 Docker'ın küçük init process'i (docker-init), shell sıradan bir process ve sinyal varsayılan davranışıyla onu öldürdü. Bu kural .NET uygulamalarının docker stop'ta nasıl davrandığını da belirliyor.

PID 1 Problemi ve .NET 10

Import aracının ilk sürümü sinyallerle ilgilenmeyen basit bir console uygulaması. Kapanırken import durumunu kaydetsin diye ProcessExit'e bir handler bağlı:

// İlk sürüm: sinyal yönetimi yok.
AppDomain.CurrentDomain.ProcessExit += (_, _) => Console.WriteLine("ProcessExit: flushing import state");

const int BatchCount = 30;
for (int batch = 1; batch <= BatchCount; batch++)
{
    Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} batch {batch}/{BatchCount} imported");
    Thread.Sleep(1000);
}

Console.WriteLine("Import finished");

Henüz Dockerfile yazmadığımız için (5. yazının konusu) uygulamayı host'ta publish edip çıktısını runtime image'ına bağlıyoruz. Aynı kodu bir de net9.0 hedefiyle derleyip .NET 9 runtime image'ında çalıştırdım:

dotnet publish -c Release -o out
docker run -d --name dockerseries-tools -v "${PWD}/out:/app" -w /app mcr.microsoft.com/dotnet/runtime:10.0 dotnet Bookstore.Tools.dll
docker stop dockerseries-tools
docker logs --tail 2 dockerseries-tools
docker inspect -f "ExitCode={{.State.ExitCode}}" dockerseries-tools
# .NET 10
10:32:05 batch 3/30 imported
10:32:06 batch 4/30 imported
ExitCode=143
# .NET 9
10:32:22 batch 3/30 imported
ProcessExit: flushing import state
ExitCode=143

İkinci sorunun cevabı: .NET 10'da handler çalışmıyor. Bu .NET 10'un belgelenmiş bir breaking change'i. Microsoft'un sayfasına göre runtime artık Unix'te varsayılan bir SIGTERM handler'ı kaydetmiyor; işletim sisteminin varsayılan davranışı process'i hemen sonlandırıyor ve ProcessExit ile AssemblyLoadContext.Unloading event'leri tetiklenmiyor. .NET 9'da runtime'ın kendi handler'ı bu event'leri tetikleyip çıkıyordu. Exit code iki durumda da 143; fark, kapanış kodunun çalışıp çalışmadığında. dotnet burada PID 1 olduğu hâlde sinyal yok sayılmadı, çünkü runtime'ın native katmanı sinyali önce kendisi yakalıyor ve .NET 10'da ardından işletim sisteminin varsayılan davranışını uyguluyor. Microsoft'un önerisi iki yol: ASP.NET Core ve Generic Host kullanan uygulamalar zaten kendi handler'larını kaydediyor; host kullanmayan uygulama PosixSignalRegistration.Create(PosixSignal.SIGTERM, ...) ile kendi handler'ını kurar.

Şimdi ilk soruya gelelim: aynı uygulama bir shell üzerinden başlatılırsa ne olur?

docker run -d --name dockerseries-tools -v "${PWD}/out:/app" -w /app mcr.microsoft.com/dotnet/runtime:10.0 sh -c 'dotnet Bookstore.Tools.dll; echo import done'
docker top dockerseries-tools -o pid,args
time docker stop dockerseries-tools
docker inspect -f "ExitCode={{.State.ExitCode}}" dockerseries-tools
PID                 COMMAND
892                 sh -c dotnet Bookstore.Tools.dll; echo import done
907                 dotnet Bookstore.Tools.dll
dockerseries-tools
real	0m 10.11s
user	0m 0.01s
sys	0m 0.00s
ExitCode=137

Container'ın PID 1'i artık dotnet değil sh. docker stop'un SIGTERM'ü PID 1'e, yani shell'e gidiyor; shell'in bu sinyal için handler'ı olmadığından kernel sinyali yok sayıyor ve dotnet sinyali hiç görmüyor. Docker 10 saniye bekliyor, sonra SIGKILL atıyor: cevap "10 saniye ve 137". Her deploy'da her container için 10 saniye kaybedersin ve uygulama hiçbir zaman temiz kapanmaz.

Bunun üç çözümü var ve her birinin sınırı farklı:

# 1. exec: shell kendini dotnet ile değiştirir, dotnet PID 1 olur
sh -c 'exec dotnet Bookstore.Tools.dll'
# 2. Dockerfile'da exec form: shell hiç araya girmez (5. yazı)
ENTRYPOINT ["dotnet", "Bookstore.Tools.dll"]
# 3. --init: PID 1 docker-init olur ve sinyali doğrudan child'ına iletir
docker run --init ...

exec ile denediğimde dotnet PID 1 oldu ve docker stop 0,11 saniyede bitti (exit code 143). --init'i shell'li komutla denediğimde de stop 0,12 saniye sürdü, ama dikkat: docker-init sinyali yalnız kendi child'ı olan shell'e iletti, shell öldü ve container kapanırken dotnet process'i namespace'le birlikte SIGKILL ile yok oldu. Yani --init sinyali shell'in arkasındaki uygulamaya taşımaz; zombi process'leri toplar ve PID 1 korumasını kaldırır, o kadar. Doğru çözüm uygulamanın kendisini PID 1 yapmak (exec form) ve uygulamanın SIGTERM'e cevap vermesi.

Generic Host ile Graceful Shutdown

Import aracını Generic Host'a taşıyalım. Host'un varsayılan lifetime'ı ConsoleLifetime; dokümana göre SIGINT, SIGQUIT ve SIGTERM'ü dinler ve host'u düzgün durdurmaya çalışır. .NET 10'daki değişikliği bizim yerimize telafi eden de bu:

using Bookstore.Tools;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddSimpleConsole(options =>
{
    options.SingleLine = true;
    options.TimestampFormat = "HH:mm:ss ";
});
builder.Services.Configure<HostOptions>(options => options.ShutdownTimeout = TimeSpan.FromSeconds(8));
builder.Services.AddHostedService<ImportWorker>();

using IHost host = builder.Build();
await host.RunAsync();
namespace Bookstore.Tools;

public sealed class ImportWorker(ILogger<ImportWorker> logger, IHostApplicationLifetime lifetime) : BackgroundService
{
    private const int BatchCount = 30;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        int batch = 0;
        try
        {
            while (batch < BatchCount)
            {
                batch++;
                await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
                logger.LogInformation("Batch {Batch}/{Total} imported", batch, BatchCount);
            }

            logger.LogInformation("Import finished");
        }
        catch (OperationCanceledException)
        {
            logger.LogWarning("Import stopped during batch {Batch}; next run resumes from batch {Batch}", batch, batch);
        }
        finally
        {
            lifetime.StopApplication();
        }
    }
}

Proje Microsoft.Extensions.Hosting 10.0.15 paketini kullanıyor; Microsoft.Extensions.DependencyInjection, Hosting ve Logging namespace'leri GlobalUsings.cs'te. Import bitince StopApplication host'u kapatıyor, yani araç tek seferlik bir iş olarak da çalışıyor. Aynı docker run ve docker stop:

10:33:11 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.
10:33:11 info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production
10:33:11 info: Microsoft.Hosting.Lifetime[0] Content root path: /app
10:33:12 info: Bookstore.Tools.ImportWorker[0] Batch 1/30 imported
10:33:13 info: Bookstore.Tools.ImportWorker[0] Batch 2/30 imported
10:33:14 info: Bookstore.Tools.ImportWorker[0] Batch 3/30 imported
10:33:15 info: Microsoft.Hosting.Lifetime[0] Application is shutting down...
10:33:15 warn: Bookstore.Tools.ImportWorker[0] Import stopped during batch 4; next run resumes from batch 4
ExitCode=0

docker stop 0,13 saniyede bitti, worker iptali gördü, yarım kalan batch'i log'ladı ve process 0 ile çıktı. ShutdownTimeout'u 8 saniyeye çekmemin nedeni şu: varsayılanı 30 saniye, Docker'ın bekleme süresi 10 saniye. Host'un kapanma süresi Docker'ınkinden uzunsa, yavaş kapanan bir hosted service'i Docker yarıda SIGKILL ile keser ve kapanış kodun yine yarım kalır. Aynı hesap Kubernetes'te terminationGracePeriodSeconds için geçerli; Kubernetes serisinin "Graceful Shutdown: SIGTERM, terminationGracePeriodSeconds, preStop ve .NET HostOptions" yazısı bunu ayrıntılı anlatıyor. Bu kodun shell'li sürümle denenmesi de beklediğimiz sonucu verdi: 10,12 saniye ve 137, Application is shutting down satırı hiç yok.

Restart Policy'leri

Docker çöken bir container'ı kendisi yeniden başlatabilir. Dört policy var: no (varsayılan), on-failure[:N] (sıfırdan farklı exit code'da, en fazla N kez), always ve unless-stopped. Hep başarısız olan bir job'la on-failure'ı görelim:

docker run -d --name dockerseries-retry --restart on-failure:3 alpine:3 sh -c "echo \"\$(date +%T) started\"; exit 1"
docker logs dockerseries-retry
docker inspect -f "RestartCount={{.RestartCount}} Status={{.State.Status}} ExitCode={{.State.ExitCode}}" dockerseries-retry
10:34:17 started
10:34:17 started
10:34:17 started
10:34:18 started
RestartCount=3 Status=exited ExitCode=1

Bir ilk çalışma, üç retry; aralar Docker'ın artan bekleme süresiyle uzar. always ile unless-stopped arasındaki fark ise yalnız daemon yeniden başladığında görünür. İki nginx container'ını bu iki policy ile başlatıp ikisini de docker stop ile durdurdum, sonra daemon'ı yeniden başlattım:

docker run -d --name dockerseries-always --restart always nginx:1.30-alpine
docker run -d --name dockerseries-unless --restart unless-stopped nginx:1.30-alpine
docker stop dockerseries-always dockerseries-unless
# daemon yeniden başlatıldı (Linux'ta: sudo systemctl restart docker)
docker ps -a --filter name=dockerseries- --format "table {{.Names}}\t{{.Status}}"
NAMES                 STATUS
dockerseries-unless   Exited (0) 26 seconds ago
dockerseries-always   Up 9 seconds

always, elle durdurulmuş olsa bile daemon açılınca container'ı başlatır; unless-stopped senin durdurma kararına saygı duyar. Sunucu yeniden başladığında ayağa kalkması gereken ama bakım için elle durdurabildiğin servisler için unless-stopped genellikle daha doğru seçim. Restart policy tek bir makinede çalışır ve yalnız process'in ölmesine bakar; uygulamanın cevap verip vermediğini bilmez. Health check'e göre karar veren orkestrasyonu Docker Compose ve Kubernetes serilerinde görüyoruz.

Üretimde Dikkat

Her uygulamanın docker stop'ta kaç saniyede kapandığını ölç ve bunu deploy bütçene koy. 10 saniyenin dolduğu her deploy, yarım kalmış istekler ve 137'lerle biten log'lar demek. ASP.NET Core için Kestrel'in açık istekleri bitirmesi ShutdownTimeout içinde olur; bu süreyi Docker'ın ya da orkestratörün bekleme süresinin altında tut.

Exit code'ları izle. Çöken container'ın exit code'u ve OOMKilled alanı, "neden kapandı?" sorusunun ilk cevabıdır. 137 ve OOMKilled=true bellek sınırına, 137 ve false dışarıdan öldürülmeye ya da stop süresinin dolmasına, 143 sinyali yakalamayan bir uygulamaya işaret eder.

Restart policy'yi hata gizlemek için kullanma. always ile sürekli yeniden başlayan bir container, docker ps'te Up 3 seconds gibi masum görünür; RestartCount'a bakmazsan saatlerce çöküp kalktığını fark etmezsin.

Sık Düşülen Tuzaklar

Uygulamayı bir shell script'i ya da shell form komutla başlatıp sinyali yutmak. Script gerekiyorsa son satırı exec dotnet Bookstore.Tools.dll olsun; Dockerfile'da exec form kullan.

.NET 10'a geçince host kullanmayan araçlarda kapanış kodunun sessizce çalışmayı bırakması. ProcessExit'e bağlı temizlik kodun varsa ya Generic Host'a geç ya da PosixSignalRegistration ile SIGTERM handler'ı kur.

--rm olmadan sürekli docker run yapmak. Her çalıştırma diskte bir container bırakır. 50 MB'lık rapor dosyası üreten bir job'u üç kez çalıştırınca:

docker ps -a -s --filter name=dockerseries-job --format "table {{.Names}}\t{{.Status}}\t{{.Size}}"
docker system df
NAMES               STATUS                              SIZE
dockerseries-job3   Exited (0) Less than a second ago   52.4MB (virtual 61.8MB)
dockerseries-job2   Exited (0) Less than a second ago   52.4MB (virtual 61.8MB)
dockerseries-job1   Exited (0) Less than a second ago   52.4MB (virtual 61.8MB)
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Containers      3         0         157.3MB   157.3MB (100%)

Tek seferlik işlerde --rm kullan, kalanları adıyla docker rm ile sil. Durmuş bütün container'ları silen docker container prune da var, ama paylaşılan bir makinede başkasının incelemek için bıraktığı container'ları da siler; neyi sildiğini bilerek kullan.

docker exec ile container içinde yaptığın değişikliği kalıcı sanmak. Container silinip yeniden oluşturulduğunda image'daki hâline döner.

Küçük Bir Deneme

★☆☆ nginx:1.30-alpine ile bir container başlat, docker stop ve docker kill ile ayrı ayrı durdur, her seferinde docker logs'un son satırlarına ve exit code'a bak. docker stop -s SIGTERM verince log'da ne değişiyor?

★★☆ Yazıdaki ilk import sürümüne PosixSignalRegistration.Create(PosixSignal.SIGTERM, ...) ile bir handler ekle: handler context.Cancel = true desin ve döngüye "dur" işareti versin. .NET 10 runtime image'ında docker stop sonrası exit code ve log'lar nasıl değişti?

★★★ Generic Host'lu sürümde ShutdownTimeout'u 30 saniyeye çıkar ve worker'ın iptalde 15 saniye süren bir temizlik yapmasını sağla. docker stop ile docker stop -t 20'nin sonuçlarını karşılaştır; hangi süreyi neye göre seçeceğini bir cümleyle yaz.

Container'ın nasıl başlayıp nasıl öldüğünü gördük. Bir sonraki yazıda container'ın kaynağı olan image'ın içine bakıyoruz: layer'lar, tag ve digest farkı, registry'ler, Docker Hub limitleri ve tek bir adın arkasında amd64 ile arm64 image'larını taşıyan multi-platform manifest'ler.