Multi-stage Build Derinlemesine: Target'lar, Test Stage'i, Paralel Stage'ler ve Image Boyutu
Multi-stage build'de son image yalnız son stage'in içeriğinden oluşur ve BuildKit yalnız hedefe giden stage'leri, birbirine bağlı olmayanları paralel build eder. Bookstore.Tools'u restore, build, test, export, debug ve final stage'lerine bölüp image'ı 1,34 GB'tan 143 MB'a indiriyor, test stage'inin ve chiseled image'ın sınırlarını görüyor, --platform=$BUILDPLATFORM ile QEMU'suz iki mimarili image üretiyoruz.
Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3 (BuildKit 0.34.2), Buildx 0.39.0, .NET SDK 10.0.404 ve .NET Runtime 10.0.15 image'ları, xunit.v3 4.0.3 (Microsoft.Testing.Platform) ve registry:3 ile denenmiştir. Boyutlar linux/amd64 image'larından alınmıştır.
Önce Tahmin Et
Bir Dockerfile'da beş stage var: restore, build, test, runtime ve final. test stage'i dotnet test çalıştırıyor, final ise runtime'dan türüyor ve ondan hiçbir dosya almıyor. docker build . dediğinde testler çalışır mı? İkinci soru: 6. yazının sonunda 1,34 GB olan Bookstore.Tools image'ı, SDK'yı final image'dan çıkarınca kaç MB'a iner: 600, 300 yoksa 150?
Neden Stage'lere Bölüyoruz?
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) import aracı Bookstore.Tools'un image'ı şu an her şeyi taşıyor: .NET SDK'sı, NuGet ile ilgili ara dosyalar, kaynak kod, obj/ ve bin/ klasörleri. Container'da çalışan tek şey dotnet Bookstore.Tools.dll ve bunun için runtime ile publish çıktısı yeterli. Fazlası yalnız boyut değil: image'ı çeken herkes kaynak kodu okuyabilir, SDK'daki her paketin CVE'si (Common Vulnerabilities and Exposures; açıklanmış güvenlik açıklarının kataloğu) tarama raporuna girer, saldırganın kullanabileceği araçlar (derleyici, shell, curl) hazır bekler.
Multi-stage build'in fikri basit: bir Dockerfile'da birden çok FROM yazarsın, her biri yeni bir stage başlatır ve son image yalnız son stage'in içeriğinden oluşur. Önceki stage'lerden dosya almanın tek yolu COPY --from=<stage>'dir. ASP.NET Core Web API serisinin "ASP.NET Core Web API'yi Docker'a Almak ve GitHub Actions ile CI/CD Kurmak" yazısında iki stage'li temel bir örnek görmüştük; burada aynı fikri Bookstore'da genişletiyoruz: ayrı bir test stage'i, hedef mimariye göre derleme, host'a çıktı veren bir stage ve debug için ayrı bir target.
Bookstore.Tools İçin Stage'ler
Önce testleri ekledik: tests/Bookstore.Tools.Tests, BookRow'un 13 haneli ISBN (International Standard Book Number; kitaplara verilen uluslararası numara) doğrulamasını ve 6. yazıda eklediğimiz Trim() düzeltmesini test ediyor. Dockerfile'ın tamamı:
# syntax=docker/dockerfile:1
ARG DOTNET_VERSION=10.0
# restore: SDK build makinesinin platformunda çalışır, asla QEMU altında değil
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS restore
WORKDIR /src
COPY Bookstore.slnx global.json ./
COPY src/Bookstore.Tools/Bookstore.Tools.csproj src/Bookstore.Tools/
COPY tests/Bookstore.Tools.Tests/Bookstore.Tools.Tests.csproj tests/Bookstore.Tools.Tests/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet restore Bookstore.slnx
COPY src/ src/
# build: hedef mimari için publish (cross-compilation)
FROM restore AS build
ARG TARGETARCH
ARG BUILD_CONFIGURATION=Release
ARG APP_VERSION=1.0.0
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c $BUILD_CONFIGURATION -a $TARGETARCH \
-o /app -p:Version=$APP_VERSION
# test: yalnız istendiğinde build edilir (--target test ya da bake'teki "test" target'ı)
FROM restore AS test
COPY tests/ tests/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet test --project tests/Bookstore.Tools.Tests/Bookstore.Tools.Tests.csproj --no-restore
# export: publish çıktısını host'a verir (docker build --target export --output out)
FROM scratch AS export
COPY --from=build /app /
# runtime: yalnız runtime; SDK yok, shell yok, root değil
FROM mcr.microsoft.com/dotnet/runtime:${DOTNET_VERSION}-noble-chiseled AS runtime
ARG APP_VERSION=1.0.0
WORKDIR /app
COPY --from=build /app ./
COPY --chown=app:app data/books.csv data/books.csv
LABEL org.opencontainers.image.title="Bookstore.Tools" \
org.opencontainers.image.version=$APP_VERSION
ENV DOTNET_ENVIRONMENT=Production \
Import__File=data/books.csv
USER app
ENTRYPOINT ["dotnet", "Bookstore.Tools.dll"]
CMD ["--Import:DelayMs=500"]
# debug: runtime + başka bir image'dan alınan statik busybox
FROM runtime AS debug
COPY --from=busybox:1.37-musl /bin/busybox /busybox/busybox
# final: son stage varsayılan target'tır, bu yüzden production image'ı olmalı
FROM runtime AS final
Birkaç ayrıntı:
ARG DOTNET_VERSIONilkFROM'dan önce tanımlı; böyle birARGyalnızFROMsatırlarında kullanılabilir ve .NET sürümünü tek yerden değiştirmeyi sağlar. Stage'in içinde kullanmak istersen orada yenidenARGyazman gerekir;APP_VERSION'ı iki stage'de ayrı ayrı tanımlamamızın nedeni bu.AS restore,AS buildgibi adlar stage'lere verilir veFROM restore AS buildbir stage'i diğerinin üzerine kurar.buildvetestaynırestorestage'inden türüyor, yani restore bir kez yapılıyor.global.jsonilkCOPY'de, çünküdotnet test'in hangi test platformuyla çalışacağını o belirliyor (birazdan). 6. yazıdaki kural geçerli: restore'u ve SDK davranışını etkileyen dosyalar kaynak koddan önce gelir.runtimeimage'ı Ubuntu 24.04 tabanlı chiseled varyant: yalnız .NET runtime'ının ihtiyaç duyduğu paketler var, shell ve paket yöneticisi yok,appkullanıcısı tanımlı. Image ailelerini 8. yazıda ayrıntılı karşılaştıracağız.
Hangi Stage'ler Çalışır? Target'lar ve Paralellik
BuildKit Dockerfile'ı yukarıdan aşağıya satır satır çalıştırmaz; hedef stage'den geriye doğru bir bağımlılık grafiği çıkarır ve yalnız o hedefe giden stage'leri build eder. Hedef verilmezse son stage hedeftir. Bu yüzden ilk sorunun cevabı hayır:
docker build --progress=plain -f src/Bookstore.Tools/Dockerfile -t dockerseries-bookstore-tools:1.0.0 .
#8 [restore 1/7] FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:fe0fd4e93bd2fbb47c883b7a6369e5c8d3db8bf01cdc5981f6ee35fbdf3ff4fb
#9 [runtime 1/4] FROM mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled@sha256:1b234cef150624fe8cc7d027e8d742c0f03a16aef2484dc469eb8c872167255e
#14 [restore 6/7] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet restore Bookstore.slnx
#14 CACHED
#15 [restore 7/7] COPY src/ src/
#16 [build 1/1] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c Release -a amd64 -o /app -p:Version=1.0.0
#16 0.629 Restored /src/src/Bookstore.Tools/Bookstore.Tools.csproj (in 125 ms).
#16 DONE 1.3s
#17 [runtime 2/4] WORKDIR /app
#18 [runtime 3/4] COPY --from=build /app ./
#19 [runtime 4/4] COPY --chown=app:app data/books.csv data/books.csv
#20 naming to docker.io/library/dockerseries-bookstore-tools:1.0.0 done
Çıktıda test, export ve debug yok; BuildKit onları hiç çalıştırmadı. final stage'inin kendi adımı olmadığı için log'da adımlar runtime adıyla görünüyor. TARGETARCH hedef platformdan geldiği için publish -a amd64 ile çalıştı. Publish'in içindeki kısa restore (125 ms), -a verildiğinde SDK'nın hedef mimari için yaptığı ek restore; paketler cache mount'tan geldiği için hızlı. Bir de paralellik var: birbirine bağlı olmayan stage'ler aynı anda çalışır. Bu çıktıda restore cache'ten geldiği için görünmüyor; ilk build'de runtime image'ının indirilmesi restore'la aynı anda başlamış ve ondan önce bitmişti. runtime stage'i yalnız COPY --from=build satırında build'i bekler.
Testleri çalıştırmak için hedefi açıkça verirsin:
docker build --progress=plain --target test -f src/Bookstore.Tools/Dockerfile .
#14 [test 1/2] COPY tests/ tests/
#15 [test 2/2] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet test --project tests/Bookstore.Tools.Tests/Bookstore.Tools.Tests.csproj --no-restore
#15 2.478 Test run summary: Passed!
#15 2.478 total: 6
#15 2.478 failed: 0
#15 2.478 succeeded: 6
#15 2.478 skipped: 0
#15 2.478 duration: 646ms
Restore stage'i önceki build'den CACHED geldi; yalnız testler çalıştı. Testler kırılırsa dotnet test sıfırdan farklı kodla çıkar ve build durur. 6. yazıdaki Bake dosyasına bir test target'ı ekleyip ci grubuna koyduk:
target "test" {
context = "."
dockerfile = "src/Bookstore.Tools/Dockerfile"
target = "test"
output = ["type=cacheonly"]
}
type=cacheonly sonucu bir image olarak kaydetmez, yalnız build cache'ine yazar; test stage'inden image istemiyoruz, yalnız başarılı olmasını istiyoruz. tools target'ına da target = "final" ekledik, böylece Dockerfile'a yeni stage eklense bile hangi image'ın üretileceği açık. Grubu çalıştırınca test ve build aynı oturumda yan yana ilerliyor:
docker buildx bake ci
#13 [test restore 6/7] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet restore Bookstore.slnx
#22 [test test 1/2] COPY tests/ tests/
#23 [tools runtime 1/4] FROM mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled@sha256:1b234cef150624fe8cc7d027e8d742c0f03a16aef2484dc469eb8c872167255e
#24 [tools build 1/1] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet publish ...
#25 [test test 2/2] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet test --project tests/Bookstore.Tools.Tests/Bookstore.Tools.Tests.csproj --no-restore
#25 2.336 Test run summary: Passed!
#28 [tools runtime 4/4] COPY --chown=app:app data/books.csv data/books.csv
#29 naming to docker.io/library/dockerseries-bookstore-tools:1.0.0 done
lint
Check complete, no warnings found.
#24 publish ve #25 test aynı anda başladı; ortak restore stage'i bir kez çalıştı. Dikkat: tools target'ı testin sonucunu beklemiyor. Bunu denemek için HasValidIsbn13'teki digit * 3'ü bilerek digit * 2 yaptık ve docker buildx bake ci'yi yeniden çalıştırdık:
#29 naming to docker.io/library/dockerseries-bookstore-tools:1.0.0 done
#24 2.125 Test run summary: Failed!
#24 2.125 failed: 2
ERROR: target test: failed to solve: process "/bin/sh -c dotnet test --project tests/Bookstore.Tools.Tests/Bookstore.Tools.Tests.csproj --no-restore" did not complete successfully: exit code: 2
Bake hata verdi, ama dockerseries-bookstore-tools:1.0.0 tag'i o arada hatalı kodla build edilen yeni image'a geçmişti (docker image ls'te image ID'si değişti). "Test geçmeden image yok" kuralı istiyorsan bunu pipeline'ın adım sırasıyla kurarsın: önce test target'ı, başarılıysa image. Final image'ı yapay olarak test stage'ine bağlamak (ör. test stage'inden boş bir dosya kopyalamak) da çalışır, ama paralelliği kaybettirir.
Test Stage'i ve Neden CI'da Ayrı Bir Job Hâlâ Gerekli
Test projesi xunit.v3 kullanıyor. xunit.v3'ün 4.x sürümü Microsoft.Testing.Platform (MTP; Microsoft'un VSTest'in yerine getirdiği yeni test çalıştırma altyapısı) üzerinde çalışıyor ve .NET 10 SDK'da dotnet test'in bu modda çalışması için repository köküne bir global.json gerekiyor. İlk denemede bunu unuttuk ve test stage'i şu hatayla durdu:
error : Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. If you use dotnet test, you should opt-in to the new dotnet test experience. For more information, see https://aka.ms/dotnet-test-mtp-error
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
Bu modda dotnet test proje yolunu --project ile alıyor. Testlerin kendisi sıradan xUnit testleri:
using Bookstore.Tools;
using Xunit;
namespace Bookstore.Tools.Tests;
public sealed class BookRowTests
{
[Theory]
[InlineData("9780441172719", true)]
[InlineData("9780141439587", true)]
[InlineData("9780141439510", false)]
[InlineData("978014143958", false)]
[InlineData("97801414395AB", false)]
public void HasValidIsbn13_checks_length_digits_and_check_digit(string isbn, bool expected)
{
BookRow row = new(isbn, "Any", "Any", 100m);
Assert.Equal(expected, row.HasValidIsbn13());
}
[Fact]
public void Parse_trims_fields_and_reads_invariant_price()
{
BookRow row = BookRow.Parse("9780441172719, Dune ,Frank Herbert,420.50");
Assert.Equal("Dune", row.Title);
Assert.Equal(420.50m, row.PriceTry);
}
}
Test stage'inin iki faydası var: testler production image'ıyla aynı SDK sürümünde, aynı restore sonucuyla çalışır ve test bağımlılıkları (xunit, test projesi, bin/Debug klasörü) final image'a hiç girmez. Yine de testleri CI'da ayrı bir job'da (dotnet test doğrudan runner'da) koşmayı tercih ediyoruz. Nedenleri pratik: test sonuç dosyaları ve coverage raporu build container'ının içinde kalır ve dışarı almak için ek --output ayarı gerekir; başarısız bir testin çıktısı BuildKit log'unda kesilerek görünür; veritabanı isteyen integration testleri docker build içinde çalışamaz, çünkü build adımlarının container başlatma yetkisi yok (bunlar Integration ve E2E (uçtan uca) Test serisindeki Testcontainers konusu). Test stage'i en çok "Dockerfile'ın build ettiği kod gerçekten test edilen kod mu?" sorusunu kapatmak ve lokal geliştirmede tek komutla her şeyi koşmak için işe yarıyor.
COPY --from, FROM scratch ve Distroless Fikri
COPY --from yalnız stage adı almaz; bir image referansı da alabilir. debug stage'inde busybox:1.37-musl image'ından tek bir dosyayı, statik derlenmiş busybox binary'sini aldık. Neden gerektiğini chiseled image'da görüyoruz:
docker run --rm --entrypoint sh dockerseries-bookstore-tools:1.0.0
docker build --target debug -f src/Bookstore.Tools/Dockerfile -t dockerseries-bookstore-tools:1.0.0-debug .
docker run --rm --entrypoint /busybox/busybox dockerseries-bookstore-tools:1.0.0-debug ls -ln /app/data
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "sh": executable file not found in $PATH
total 4
-rw-r--r-- 1 1654 1654 314 Nov 14 11:01 books.csv
Production image'ında shell yok; içine docker exec ile girip bakamazsın. Bu bilinçli: shell olmayan bir image'da saldırganın ele geçirdiği bir process'in kullanabileceği araç da yok. Bakmak gerektiğinde ayrı bir debug target'ı build edersin; production'a giden image değişmez. Shell'siz image'ı hiç yeniden build etmeden debug etmenin yollarını (docker debug, aynı namespace'lere bağlanan yardımcı container) 14. yazıda göreceğiz.
Aynı mantıkla araç taşımak yaygın bir kullanım: başka bir image'dan statik bir healthcheck binary'si, bir sertifika paketi ya da bir CLI aracı almak. Kural, kopyaladığın dosyanın hedef image'da çalışabilmesi: dinamik bağlı bir binary, hedefte olmayan bir libc'ye ihtiyaç duyarsa çalışmaz. busybox'ın musl varyantını bu yüzden seçtik.
FROM scratch boş bir image demek: içinde hiçbir dosya yok. export stage'i bunu bir build çıktısını host'a almak için kullanıyor. --output ile BuildKit, hedef stage'in dosya sistemini image yerine bir klasöre yazar:
docker build --target export --output type=local,dest=out -f src/Bookstore.Tools/Dockerfile .
ls out | head -5
#16 exporting to client directory
#16 copying files 1.34MB 0.0s done
Bookstore.Tools
Bookstore.Tools.deps.json
Bookstore.Tools.dll
Bookstore.Tools.pdb
Bookstore.Tools.runtimeconfig.json
Bu, makinede .NET SDK'sı olmayan bir ortamda (ör. yalnız Docker'ı olan bir build agent'ında) derleme yapıp çıktıyı almanın kısa yolu. out/ klasörü 5. yazıdan beri .dockerignore'da, böylece bir sonraki build'in context'ine girmiyor. scratch final image olarak da kullanılır, ama yalnız hiçbir şeye bağımlı olmayan statik binary'ler için (Go programları gibi). .NET runtime'ı bir libc'ye ve birkaç sistem kütüphanesine ihtiyaç duyduğu için .NET'te bu fikrin karşılığı distroless image'lar: paket yöneticisi ve shell'i olmayan, yalnız uygulamanın çalışması için gereken dosyaları taşıyan image'lar. Microsoft'un Ubuntu chiseled ve Azure Linux distroless image'ları bu sınıfta.
Image Boyutu: Ne Kazandık?
İkinci sorunun cevabı. Bütün image'lar linux/amd64 için docker image ls --tree ile ölçüldü:
| Image | DISK USAGE | CONTENT SIZE |
|---|---|---|
sdk:10.0 | 1.31GB | 359MB |
runtime:10.0 (Ubuntu 24.04) | 301MB | 84.6MB |
runtime:10.0-noble-chiseled | 141MB | 43.5MB |
runtime-deps:10.0-noble-chiseled | 20.4MB | 5.95MB |
| Bookstore.Tools, 6. yazı (tek stage, SDK) | 1.34GB | 367MB |
Bookstore.Tools, multi-stage (final) | 143MB | 44.1MB |
Bookstore.Tools, debug target'ı | 145MB | 44.9MB |
Cevap 150'ye en yakın olanı: 1,34 GB'tan 143 MB'a, yaklaşık dokuzda bir. Registry'den inen sıkıştırılmış boyut (CONTENT SIZE) 367 MB'tan 44,1 MB'a indi; bu, her deploy'da ve her yeni node'da çekilen veri. containerd image store'da DISK USAGE sıkıştırılmış içerikle açılmış layer'ların toplamı; bizim image için 143 MB yaklaşık 99 MB açılmış dosya ve 44 MB sıkıştırılmış içerikten oluşuyor. Tabloda dikkat çeken bir şey daha var: bizim uygulamamız base image'a yalnız 1,4 MB ekliyor. Image boyutunu artık neredeyse tamamen base image seçimi belirliyor. runtime-deps ise .NET runtime'ını bile içermiyor; self-contained ya da Native AOT (Ahead-of-Time; uygulamayı önceden makine koduna derleme) ile derlenmiş uygulamalar için.
Layer bazında nereden ne geldiğini görmek için 5. yazıdaki gibi docker history yeterli:
docker history dockerseries-bookstore-tools:1.0.0
IMAGE CREATED CREATED BY SIZE COMMENT
b3faf6b94f0d About a minute ago CMD ["--Import:DelayMs=500"] 0B buildkit.dockerfile.v0
<missing> About a minute ago ENTRYPOINT ["dotnet" "Bookstore.Tools.dll"] 0B buildkit.dockerfile.v0
<missing> About a minute ago USER app 0B buildkit.dockerfile.v0
<missing> About a minute ago ENV DOTNET_ENVIRONMENT=Production Import__Fi… 0B buildkit.dockerfile.v0
<missing> About a minute ago LABEL org.opencontainers.image.title=Booksto… 0B buildkit.dockerfile.v0
<missing> About a minute ago COPY --chown=app:app data/books.csv data/boo… 16.4kB buildkit.dockerfile.v0
<missing> About a minute ago COPY /app ./ # buildkit 1.41MB buildkit.dockerfile.v0
<missing> About a minute ago WORKDIR /app 8.19kB buildkit.dockerfile.v0
<missing> About a minute ago ARG APP_VERSION=1.0.0 0B buildkit.dockerfile.v0
<missing> 3 days ago CMD ["--info"] 0B buildkit.dockerfile.v0
<missing> 3 days ago ENTRYPOINT ["/usr/bin/dotnet"] 0B buildkit.dockerfile.v0
<missing> 3 days ago COPY /dotnet-symlink /usr/bin # buildkit 12.3kB buildkit.dockerfile.v0
<missing> 3 days ago COPY /usr/share/dotnet /usr/share/dotnet # b… 83.2MB buildkit.dockerfile.v0
<missing> 3 days ago ENV DOTNET_VERSION=10.0.15 0B buildkit.dockerfile.v0
<missing> 3 days ago USER 1654 0B buildkit.dockerfile.v0
<missing> 3 days ago COPY /rootfs/home/app /home/app # buildkit 12.3kB buildkit.dockerfile.v0
<missing> 3 days ago ENV APP_UID=1654 ASPNETCORE_HTTP_PORTS=8080 … 0B buildkit.dockerfile.v0
<missing> 3 days ago COPY /rootfs / # buildkit 14.4MB buildkit.dockerfile.v0
Base image'ın kendisi de multi-stage build edilmiş: COPY /rootfs / ve COPY /usr/share/dotnet satırları Microsoft'un Dockerfile'ında başka stage'lerden kopyalanan içerik. 14,4 MB'lık kök dosya sistemi ve 83,2 MB'lık .NET runtime'ı, üstüne bizim 1,41 MB'lık publish çıktımız. Layer'ın içinde hangi dosyanın ne kadar yer tuttuğunu görmek istersen dive gibi araçlar layer'ları dosya ağacı olarak gösterir; ama çoğu soruya docker history cevap veriyor.
QEMU'suz Çok Mimarili Build
6. yazıda .NET SDK'sını QEMU (başka mimarinin komutlarını yazılımla çalıştıran emülatör) altında çalıştırmamamız gerektiğini söylemiştik. Bu Dockerfile'da bunu üç satır sağlıyor. FROM --platform=$BUILDPLATFORM, SDK stage'lerini her zaman build makinesinin mimarisinde çalıştırır. ARG TARGETARCH, BuildKit'in her hedef platform için doldurduğu mimari adını (amd64, arm64) alır ve dotnet publish -a $TARGETARCH o mimari için çıktı üretir. Final stage'deki FROM'da --platform olmadığı için o stage hedef platformun runtime image'ını kullanır. .NET SDK'sı başka mimari için derlemeyi kendisi yapabildiği için emülasyon gerekmiyor. Bake dosyasındaki tools-release target'ına iki platform ekledik:
target "tools-release" {
inherits = ["tools"]
platforms = ["linux/amd64", "linux/arm64"]
tags = ["${REGISTRY}/bookstore/tools:${TAG}"]
cache-from = ["type=registry,ref=${REGISTRY}/bookstore/tools:buildcache"]
cache-to = ["type=registry,ref=${REGISTRY}/bookstore/tools:buildcache,mode=max"]
output = ["type=registry"]
}
docker buildx bake tools-release
docker buildx imagetools inspect localhost:5000/bookstore/tools:1.0.0
docker run --rm --platform linux/arm64 localhost:5000/bookstore/tools:1.0.0
Name: localhost:5000/bookstore/tools:1.0.0
Name: localhost:5000/bookstore/tools:1.0.0@sha256:b364362899ccf588a2638eb9ffaf9122923849efc85e7827bc08e4c65a15db73
Platform: linux/amd64
Name: localhost:5000/bookstore/tools:1.0.0@sha256:efccdbf15b9c0646577649efe26e5a985dff50f12df34b3c2181e2479d75a146
Platform: linux/arm64
...
11:22:24 info: Bookstore.Tools.ImportWorker[0] Import finished: 5 books in 3 batches
Build log'unda restore stage'i bir kez çalışıyor; publish adımı ise her hedef için ayrı satırda, [<build platformu>-><hedef> build 1/1] biçiminde görünüyor ve biri -a amd64, diğeri -a arm64 ile çalışıyor. arm64 image'ını x64 bir makinede çalıştırdığında runtime'ın kendisi QEMU ile çalışır; bu deneme için kabul edilebilir, ama arm64 image'ını asıl test edeceğin yer arm64 bir makine. Aynı işi Dockerfile'sız, SDK'nın kendi container desteğiyle yapmayı 9. yazıda göreceğiz.
Bu yaklaşımın bir sınırı var: -a ile publish, uygulama native bir kütüphane içeren bir NuGet paketi kullanıyorsa (ör. SQLite ya da bir görüntü işleme kütüphanesi) hedef mimarinin native dosyasını seçer, bu doğru davranış. Ama build stage'inde hedef mimari için bir şey çalıştırman gerekirse (ör. hedef mimariye özel bir aracı RUN ile çağırmak) o adım yine QEMU'ya düşer. .NET'te bu nadir; Angular build'inde ise hiç gerekmez, çünkü çıktı mimariden bağımsız JavaScript.
Üretimde Dikkat
Final stage'i ve hangi stage'in hangi image'ı ürettiğini açık tut. Bake'te ya da pipeline'da her zaman --target final (ya da target = "final") ver; Dockerfile'a birisi en alta yeni bir stage eklediğinde production'a giden image sessizce değişmesin.
Production image'ına SDK, test projesi, kaynak kod ya da .pdb dışında bir build artığı girmediğini kontrol et. docker history'de beklenmedik bir büyük layer ya da docker run --entrypoint ile listelenen beklenmedik dosyalar bunun işaretidir. .pdb dosyaları stack trace'lerde satır numarası verdiği için çoğu ekip bunları bilerek bırakır; bırakmak istemiyorsan publish'e -p:DebugType=none ekleyebilirsin.
Base image'ı uygulamanın gerçek ihtiyacına göre seç. Chiseled image'larda shell, curl, ICU (International Components for Unicode; kültüre özel sıralama ve büyük-küçük harf kurallarını sağlayan kütüphane) ve saat dilimi verisi yok. Bookstore.Tools için sorun değil; Türkçe sıralama yapan ya da Europe/Istanbul saat dilimini kullanan Bookstore.Api için bunun nasıl bir hataya döndüğünü bir sonraki yazıda göreceğiz.
Debug target'ını registry'ye ayrı bir tag'le (ör. 1.0.0-debug) gönder ya da hiç gönderme. Aynı tag'i paylaşırlarsa production'da hangisinin çalıştığını bilemezsin.
Sık Düşülen Tuzaklar
Final stage'i SDK stage'inden türetmek. FROM build AS final yazmak multi-stage'in bütün kazancını götürür ve image yeniden 1 GB'ın üzerine çıkar. Final stage'in FROM'u her zaman bir runtime image'ı olmalı.
Stage'lere ad yerine index ile başvurmak. COPY --from=0 ilk stage'i gösterir; Dockerfile'ın başına yeni bir stage eklenince aynı satır sessizce başka bir stage'den kopyalamaya başlar. Her stage'e AS ile ad ver ve adıyla başvur.
Debug ya da test stage'ini en alta yazmak. Hedef verilmeyen bir docker build son stage'i build eder; en altta debug varsa production'a busybox'lı image gider. Bu yüzden Dockerfile'ımızda son stage final.
ARG'ı FROM'dan önce tanımlayıp stage'in içinde kullanmaya çalışmak. İlk FROM'dan önceki ARG yalnız FROM satırlarında geçerli. Küçük bir denemede (ARG DOTNET_VERSION=10.0, ardından FROM alpine:3 ve RUN echo "version=$DOTNET_VERSION") docker build --check "no warnings found" dedi, build'in çıktısı ise version= oldu: değer boş geldi. Stage'in içinde gerekiyorsa ARG DOTNET_VERSION'ı orada değersiz olarak yeniden yaz; varsayılan değeri yukarıdan alır.
Test stage'inin her build'de çalıştığını sanmak. Yukarıda gördüğümüz gibi BuildKit yalnız hedefe giden stage'leri build eder. Testlerin CI'da gerçekten çalıştığından emin olmak için onları ayrı bir adım ya da Bake target'ı olarak çağır.
Küçük Bir Deneme
★☆☆ BookRow.HasValidIsbn13'teki digit * 3'ü digit * 2 yap. Önce docker build ., sonra docker build --target test . çalıştır. Hangisi hatayı yakaladı ve neden?
★★☆ Final stage'in base image'ını sırayla runtime:10.0, runtime:10.0-alpine ve runtime:10.0-noble-chiseled yap; her birinde docker image ls --tree ile linux/amd64 boyutunu ölç ve uygulamanın üçünde de çalıştığını kontrol et.
★★★ build stage'inde publish'i -p:PublishSingleFile=true --self-contained ile yap ve final image'ı runtime-deps:10.0-noble-chiseled üzerine kur. ENTRYPOINT'i nasıl değiştirmen gerekti, image ne kadar küçüldü ya da büyüdü?
Bookstore.Tools artık 143 MB'lık, shell'siz, root olmayan ve iki mimari için QEMU'suz build edilen bir image. Bir sonraki yazıda aynı yaklaşımı asıl uygulamaya, Bookstore.Api'ye uyguluyoruz: ASP.NET Core image aileleri, port 8080, environment variable ile configuration ve chiseled image'da Türkçe sıralamanın ve saat diliminin neden bozulduğu.