Build Cache ve BuildKit: Layer Sırası, Cache Mount, Build Secret'ları ve Bake
Build cache, bir adımın girdisi değişmedikçe onu yeniden çalıştırmaz; COPY'de girdi dosya içeriğidir, bu yüzden önce csproj'u kopyalayıp restore'u kaynak koddan ayırırsan kod değişikliği paketleri yeniden indirmez. Bookstore.Tools'ta build'i 4,6 sn'den 1,6 sn'ye indiriyor, NuGet cache mount'unu ölçüyor, private feed token'ını secret mount ile sızdırmadan veriyor, QEMU ile multi-platform build'e ve docker buildx bake'e bakıyoruz.
Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3 (BuildKit 0.34.2), Buildx 0.39.0, mcr.microsoft.com/dotnet/sdk:10.0 image'ı (SDK 10.0.404), nginx:1.30-alpine ve registry:3 (Distribution 3.1.2) ile denenmiştir.
Önce Tahmin Et
Bir önceki yazıda Bookstore.Tools için yazdığımız Dockerfile'da ImportWorker.cs'de tek bir log satırını değiştirip image'ı yeniden build ediyorsun. Kaynak kodun dışında hiçbir şey değişmedi: ne csproj ne paket sürümleri. Build sırasında NuGet paketleri yeniden indirilir mi? İkinci soru: build'in bir adımı private bir NuGet feed'inden paket çekmek için token istiyor. Token'ı image'a, geçmişine ya da build cache'ine hiç yazmadan o adıma nasıl verirsin?
BuildKit Cache'i Nasıl Kullanır?
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) import aracı Bookstore.Tools'ta küçük bir değişiklik yaptık: import bitince kaç kitabın kaç batch'te yazıldığını log'luyoruz. Ardından 5. yazıdaki Dockerfile ile yeniden build:
docker build --progress=plain -f src/Bookstore.Tools/Dockerfile -t dockerseries-bookstore-tools:1.0.0 .
#8 [2/7] WORKDIR /src
#8 CACHED
#9 [3/7] COPY Bookstore.slnx ./
#9 CACHED
#10 [4/7] COPY src/ src/
#10 DONE 0.0s
#11 [5/7] RUN dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c Release -o /app -p:Version=1.0.0
#11 2.793 Restored /src/src/Bookstore.Tools/Bookstore.Tools.csproj (in 2.35 sec).
#11 DONE 3.5s
#12 [6/7] WORKDIR /app
#13 [7/7] COPY --chown=app:app data/books.csv data/books.csv
İlk sorunun cevabı: evet. Restored ... (in 2.35 sec) satırı restore'un baştan yapıldığını, image'da paket olmadığı için de bütün paketlerin yeniden indirildiğini söylüyor. Nedenini anlamak için BuildKit'in her adımda sorduğu soruyu bilmek yeterli: "Bu adımı daha önce aynı girdilerle çalıştırdım mı?" Girdiler instruction'ın türüne göre değişir:
RUN,ENV,WORKDIRgibi instruction'larda girdi, instruction'ın metni ve bir önceki adımın sonucudur. Komutun ürettiği sonuca bakılmaz;RUN apt-get updatemetni aynı kaldıkça cache'ten gelir.COPYveADD'de kopyalanan dosyaların içeriği de girdidir. BuildKit dosyaların checksum'ını hesaplar; tarih değil içerik önemlidir, yanitouchcache'i kırmaz ama tek bir karakter kırar.- Bir adımın cache'i kırıldığında ondan sonraki bütün adımlar yeniden çalışır, çünkü her adımın girdisi bir öncekinin sonucudur.
5. yazıdaki Dockerfile'da COPY src/ src/ bütün kaynak kodu tek seferde kopyalıyor ve hemen ardından dotnet publish hem restore hem build yapıyor. Herhangi bir .cs dosyası değişince kopyalama adımının checksum'ı değişiyor ve paketlerin restore'u da kodla birlikte yeniden çalışıyor.

Doğru Sıra: Önce csproj, Sonra Kaynak
Çözüm, seyrek değişen girdileri sık değişenlerden önce kopyalamak. Restore'un tek ihtiyacı solution ve proje dosyaları; kaynak kodu görmesi gerekmiyor. Dockerfile'ı buna göre ikiye böldük:
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0
ARG BUILD_CONFIGURATION=Release
ARG APP_VERSION=1.0.0
WORKDIR /src
# Seyrek değişen dosyalar önce: solution ve proje dosyaları
COPY Bookstore.slnx ./
COPY src/Bookstore.Tools/Bookstore.Tools.csproj src/Bookstore.Tools/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet restore src/Bookstore.Tools/Bookstore.Tools.csproj
# Kaynak kod sık değişir: restore layer'ından sonra kopyalanır
COPY src/ src/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c $BUILD_CONFIGURATION -o /app \
--no-restore -p:Version=$APP_VERSION
WORKDIR /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"]
--mount=type=cache satırlarına birazdan geleceğiz. Önce sıranın etkisi: BookRow.Parse'a alanları Trim()'leyen küçük bir düzeltme yapıp yeniden build ettik:
#10 [stage-0 4/9] COPY src/Bookstore.Tools/Bookstore.Tools.csproj src/Bookstore.Tools/
#10 CACHED
#11 [stage-0 5/9] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet restore src/Bookstore.Tools/Bookstore.Tools.csproj
#11 CACHED
#12 [stage-0 6/9] COPY src/ src/
#12 DONE 0.0s
#13 [stage-0 7/9] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c Release -o /app --no-restore -p:Version=1.0.0
#13 DONE 1.0s
Restore artık CACHED; build süresi (time ile ölçtük) 4,60 saniyeden 1,62 saniyeye indi. Bu projede tek bir paket referansı var; onlarca projeli bir solution'da restore dakikalar sürebilir ve kazanç da o ölçüde büyür. Restore yalnız csproj (ya da Directory.Packages.props, nuget.config, global.json gibi restore'u etkileyen bir dosya) değiştiğinde yeniden çalışır. Birden çok proje varsa her csproj'u kendi klasörüne ayrı COPY ile almak gerekir; bunu 8. yazıda Bookstore.Api eklenince göreceğiz. dotnet publish'e verdiğimiz --no-restore da önemli: restore'u ayrı bir layer'a aldık, publish'in onu tekrar denemesine gerek yok.
Aynı kural her ekosistemde geçerli: Angular tarafında önce package.json ve package-lock.json, ardından npm ci, en son kaynak kod. 10. yazıda bookstore-web için bunu yazacağız.
Cache Mount: Paketler Build'ler Arasında Kalsın
Layer cache'in bir sınırı var: csproj değiştiği anda restore layer'ı tamamen yeniden üretilir ve bütün paketler yeniden iner, değişen tek bir paket sürümü olsa bile. RUN --mount=type=cache bu durum için var. Belirtilen klasör (/root/.nuget/packages, SDK image'ında root kullanıcısının NuGet global packages klasörü) image'a yazılmaz; BuildKit onu build'ler arasında saklar ve aynı id ile mount edilen her adıma tekrar verir. Restore'da kullandığımız klasörün publish'te de mount edilmesi gerekiyor, çünkü paketler image'da değil o klasörde duruyor.
Etkisini görmek için bütün layer cache'ini --no-cache ile devre dışı bırakıp iki Dockerfile'ı karşılaştırdık. --no-cache layer'ları yok sayar ama cache mount'lara dokunmaz:
docker build --no-cache --progress=plain -f src/Bookstore.Tools/Dockerfile -t dockerseries-bookstore-tools:1.0.0 .
docker buildx du --filter type=exec.cachemount --verbose
#11 2.635 Restored /src/src/Bookstore.Tools/Bookstore.Tools.csproj (in 2.14 sec).
...
Size: 24.73MB
Description: cached mount /root/.nuget/packages from exec /bin/sh -c dotnet restore src/Bookstore.Tools/Bookstore.Tools.csproj with id "/nuget"
Usage count: 2
Type: exec.cachemount
Dürüst sonuç: bu projede fark küçük. Cache mount'suz Dockerfile'da restore 2,25 saniye sürdü, cache mount'lu olanda 2,14 saniye. Bookstore.Tools'un bütün bağımlılıkları 24,73 MB ve hızlı bir bağlantıda bunları indirmek restore'un asıl süresi değil. Kazanç; paket sayısı arttıkça, bağlantı yavaşladıkça ve csproj'lar sık değiştikçe büyüyor. 10. yazıda yüzlerce npm paketi olan Angular build'inde aynı ölçümü npm için de yapacağız (--mount=type=cache,target=/root/.npm). Bir de şunu unutma: cache mount builder'ın lokal diskinde durur. Her job'da sıfırdan açılan bir CI runner'ında boş başlar; orada işe yarayan, birazdan göreceğimiz cache export'tur.
Cache mount'un iki özelliği tasarımı etkiler. Birincisi, build'in cache'in içeriğine güvenmemesi gerekir: klasör boş da gelebilir, başka bir build'in bıraktığı dosyalarla dolu da. NuGet ve npm cache'leri bunun için uygun, çünkü eksik paketi indiriyorlar. İkincisi, varsayılan sharing=shared aynı cache'e aynı anda birden çok build'in yazmasına izin verir. Eşzamanlı yazmaya dayanıklı olmayan bir araç için (ör. apt'nin /var/cache/apt klasörü) sharing=locked kullanılır; ikinci build birincinin bitmesini bekler.
Build Secret: Private NuGet Feed'inden Token Sızdırmadan Restore
Bookstore ekibi ISBN (International Standard Book Number; kitaplara verilen uluslararası numara) doğrulamasını ayrı bir paket yaptı ve şirketin private feed'ine koydu: Bookstore.Isbn. Gerçek hayatta bu feed GitHub Packages (https://nuget.pkg.github.com/<owner>/index.json) ya da Azure Artifacts olur ve bir PAT (Personal Access Token; kullanıcı adına üretilen erişim anahtarı) ister. Laboratuvarda aynısını yerelde kurduk: labs/06-private-feed klasöründeki script paketi build edip statik bir NuGet v3 feed'i olarak diziyor, nginx container'ı (dockerseries-feed) da doğru token gelmeyen her isteğe 401 dönüyor. Paketi kullanan küçük IsbnCheck uygulamasının nuget.config'i:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="bookstore" value="http://feed.bookstore.test:8085/v3/index.json" allowInsecureConnections="true" />
</packageSources>
<packageSourceMapping>
<packageSource key="bookstore">
<package pattern="Bookstore.*" />
</packageSource>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
<packageSourceCredentials>
<bookstore>
<add key="Username" value="bookstore-ci" />
<add key="ClearTextPassword" value="%NUGET_TOKEN%" />
</bookstore>
</packageSourceCredentials>
</configuration>
Dosyada token yok, yalnız %NUGET_TOKEN% var: NuGet, config değerlerindeki %NAME% biçimini Linux'ta da ortam değişkeniyle değiştiriyor. Bu dosya güvenle repository'ye ve image'a girebilir. allowInsecureConnections yalnız laboratuvardaki HTTP feed için; gerçek feed HTTPS olur. Token'ı build'e veren Dockerfile:
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0
WORKDIR /src
COPY nuget.config ./
COPY IsbnCheck/IsbnCheck.csproj IsbnCheck/
# Token yalnız bu RUN çalışırken NUGET_TOKEN ortam değişkeni olarak vardır
RUN --mount=type=secret,id=nuget_token,env=NUGET_TOKEN,required=true \
dotnet restore IsbnCheck/IsbnCheck.csproj
COPY IsbnCheck/ IsbnCheck/
RUN dotnet publish IsbnCheck/IsbnCheck.csproj -c Release -o /app --no-restore
ENTRYPOINT ["dotnet", "/app/IsbnCheck.dll"]
Secret mount varsayılan olarak /run/secrets/<id> dosyası olarak bağlanır; Dockerfile frontend 1.10'dan beri env= ile doğrudan bir ortam değişkenine de verilebiliyor. NuGet'in istediği de tam olarak bir ortam değişkeni. required=true secret verilmezse adımı hemen durdurur. Önce secret'sız, sonra süresi dolmuş bir token'la, en son doğrusuyla build ettik:
docker build --add-host feed.bookstore.test=host-gateway -t dockerseries-isbncheck labs/06-private-feed
NUGET_TOKEN=expired-token docker build --add-host feed.bookstore.test=host-gateway \
--secret id=nuget_token,env=NUGET_TOKEN -t dockerseries-isbncheck labs/06-private-feed
# Laboratuvarda feed bu token'la başlatıldı
export NUGET_TOKEN=$(openssl rand -hex 16)
docker build --add-host feed.bookstore.test=host-gateway \
--secret id=nuget_token,env=NUGET_TOKEN -t dockerseries-isbncheck labs/06-private-feed
#11 ERROR: secret nuget_token: not found
error NU1301: Unable to load the service index for source http://feed.bookstore.test:8085/v3/index.json.
error NU1301: Response status code does not indicate success: 401 (Unauthorized).
#11 [stage-0 5/7] RUN --mount=type=secret,id=nuget_token,env=NUGET_TOKEN,required=true dotnet restore IsbnCheck/IsbnCheck.csproj
#11 3.230 Restored /src/IsbnCheck/IsbnCheck.csproj (in 1.63 sec).
#11 DONE 3.3s
--secret id=nuget_token,env=NUGET_TOKEN, secret'ın değerini build'i başlatan shell'in ortam değişkeninden okur; CI'da bu değişken pipeline'ın secret store'undan gelir. --add-host yalnız laboratuvar feed'inin adını çözmek için; container ağlarını 12. yazıda göreceğiz. Şimdi asıl soru, token'ın image'da iz bırakıp bırakmadığı:
docker run --rm dockerseries-isbncheck 9780441172719 9780141439510
docker history --no-trunc --format "{{.CreatedBy}}" dockerseries-isbncheck | head -5
9780441172719: valid
9780141439510: invalid
ENTRYPOINT ["dotnet" "/app/IsbnCheck.dll"]
RUN /bin/sh -c dotnet publish IsbnCheck/IsbnCheck.csproj -c Release -o /app --no-restore # buildkit
COPY IsbnCheck/ IsbnCheck/ # buildkit
RUN /bin/sh -c dotnet restore IsbnCheck/IsbnCheck.csproj # buildkit
COPY IsbnCheck/IsbnCheck.csproj IsbnCheck/ # buildkit
5. yazıdaki ARG NUGET_TOKEN denemesinde geçmişteki her RUN satırı token'ın değerini taşıyordu; burada restore satırında ne token ne de mount görünüyor. Ayrıca image'ın dosya sisteminde token değerini grep -r ile aradık, sonuç boş. Secret'ın değeri cache anahtarına da girmez: token değişse bile restore adımı, csproj aynı kaldıkça cache'ten gelir. Yeni token'la gerçekten yeniden restore istiyorsan o build'e --no-cache ver.
Aynı yaklaşımın SSH (Secure Shell; uzak makineye şifreli bağlantı protokolü) karşılığı RUN --mount=type=ssh: build'i başlatan makinedeki SSH agent'ı o adıma bağlar, docker build --ssh default ile açılır ve private bir Git repository'sini anahtar dosyasını image'a koymadan klonlamak için kullanılır. .NET projelerinde ihtiyaç nadir; paketler genellikle feed'den gelir.
Builder'lar, --platform ve QEMU
docker build aslında Buildx'in build komutu ve bir builder'a gider. Kurulu builder'ları görelim; karşılaştırmak için docker-container driver'ı ile ikinci bir builder da açtık:
docker buildx create --name dockerseries-builder --driver docker-container --bootstrap
docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
dockerseries-builder docker-container
\_ dockerseries-builder0 \_ unix:///var/run/docker.sock running v0.34.2 linux/amd64 (+2), linux/arm64, linux/arm (+2), linux/ppc64le, (3 more)
default* docker
\_ default \_ default running v0.34.2 linux/amd64 (+2), linux/arm64, linux/ppc64le, linux/s390x, (2 more)
default builder Docker Engine'in içine gömülü BuildKit'tir ve image'ları doğrudan engine'in image store'una yazar. docker-container driver'ı BuildKit'i ayrı bir container'da çalıştırır; BuildKit sürümünü engine'den bağımsız seçmek, birden çok builder'ı ayrı tutmak gibi durumlarda kullanılır. Engine 29'un yeni kurulumlarda varsayılan yaptığı containerd image store ile default builder multi-platform image ve registry cache export'u da destekliyor; eski classic store'da bunlar için docker-container builder gerekiyordu. Yani bu seride çoğu iş için default yeterli.
PLATFORMS sütunu builder'ın hangi mimariler için RUN çalıştırabildiğini gösterir. Makinenin kendi mimarisi native çalışır; diğerleri QEMU (farklı işlemci mimarisinin komutlarını yazılımla çalıştıran emülatör) ile. Docker Desktop QEMU'yu hazır kurar. Küçük bir deneme Dockerfile'ıyla iki platform için build edip lokal registry'ye gönderdik (registry'yi 4. yazıdaki gibi dockerseries-registry adıyla açtık):
# syntax=docker/dockerfile:1
FROM alpine:3
ARG TARGETPLATFORM
RUN echo "image for $TARGETPLATFORM, RUN executed on $(uname -m)" > /build-info.txt
CMD ["cat", "/build-info.txt"]
docker buildx build --platform linux/amd64,linux/arm64 \
-t localhost:5000/bookstore/platform-demo:1.0.0 --push labs/06-multi-platform
docker buildx imagetools inspect localhost:5000/bookstore/platform-demo:1.0.0
docker run --rm --platform linux/arm64 localhost:5000/bookstore/platform-demo:1.0.0
#8 [linux/amd64 2/2] RUN echo "image for linux/amd64, RUN executed on $(uname -m)" > /build-info.txt
#10 [linux/arm64 2/2] RUN echo "image for linux/arm64, RUN executed on $(uname -m)" > /build-info.txt
...
MediaType: application/vnd.oci.image.index.v1+json
Manifests:
Platform: linux/amd64
Platform: linux/arm64
Platform: unknown/unknown
Platform: unknown/unknown
image for linux/arm64, RUN executed on aarch64
TARGETPLATFORM BuildKit'in her platform için otomatik doldurduğu build argümanı; BUILDPLATFORM ise build'i yapan makinenin platformu. Her platformun RUN'ı o mimarinin binary'leriyle çalıştı; makinenin mimarisinden farklı olan QEMU üzerinden. 4. yazıda gördüğümüz index'te iki platform manifest'i ve onlara bağlı iki attestation manifest'i (unknown/unknown) var.
Alpine'de echo için QEMU sorun değil. .NET için öyle değil: 2. yazıda alıntıladığımız gibi Microsoft .NET'in QEMU altında çalışmasını desteklemiyor ve SDK'yı emülasyonla çalıştırmak hem çok yavaş hem de kırılgan. .NET image'larını çok mimarili üretmenin iki doğru yolu var. Birincisi native runner: her mimari kendi makinesinde (ör. CI'da arm64 runner) build edilir ve manifest'ler tek index'te birleştirilir. İkincisi cross-compilation: SDK build makinesinin mimarisinde çalışır (FROM --platform=$BUILDPLATFORM), dotnet publish -a $TARGETARCH hedef mimari için çıktı üretir, final image hedef mimarinin runtime'ını kullanır. Bu ikinci yol build ve final aşamalarını ayırmayı gerektirdiği için onu bir sonraki yazıda multi-stage Dockerfile ile kuracağız; SDK'nın kendi container desteğiyle çok mimarili image üretmeyi de 9. yazıda göreceğiz.
docker buildx bake: Birden Çok Target'ı Tek Komutla
Build komutları uzadıkça (Dockerfile yolu, tag, argümanlar, secret'lar, platformlar, cache ayarları) onları README'ye ya da shell script'lerine kopyalamak zorlaşıyor. Bake bu ayarları bir dosyada toplar: repository kökündeki docker-bake.hcl, HCL (HashiCorp Configuration Language; Terraform'un da kullandığı yapılandırma dili) ile yazılır:
variable "TAG" {
default = "1.0.0"
}
variable "REGISTRY" {
default = "localhost:5000"
}
group "default" {
targets = ["tools"]
}
group "ci" {
targets = ["lint", "tools"]
}
target "tools" {
context = "."
dockerfile = "src/Bookstore.Tools/Dockerfile"
args = {
APP_VERSION = TAG
}
tags = ["dockerseries-bookstore-tools:${TAG}"]
}
target "lint" {
inherits = ["tools"]
call = "check"
}
target "tools-release" {
inherits = ["tools"]
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"]
}
target bir docker build çağrısının bütün ayarlarıdır; inherits ile başka bir target'ın ayarlarını alıp yalnız farkı yazarsın. lint target'ı call = "check" ile image üretmez, 5. yazıdaki docker build --check'i çalıştırır. group birden çok target'ı birlikte çalıştırır; variable değerleri aynı adlı ortam değişkenleriyle ezilebilir (TAG=1.1.0 docker buildx bake). Ne çalışacağını build etmeden görmek için:
docker buildx bake --print tools
docker buildx bake ci
{
"group": {
"default": {
"targets": [
"tools"
]
}
},
"target": {
"tools": {
"context": ".",
"dockerfile": "src/Bookstore.Tools/Dockerfile",
"args": {
"APP_VERSION": "1.0.0"
},
"tags": [
"dockerseries-bookstore-tools:1.0.0"
]
}
}
}
...
#12 [tools stage-0 5/9] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet restore src/Bookstore.Tools/Bookstore.Tools.csproj
#16 [tools stage-0 9/9] COPY --chown=app:app data/books.csv data/books.csv
#17 naming to docker.io/library/dockerseries-bookstore-tools:1.0.0 done
lint
Check complete, no warnings found.
Bake bir gruptaki target'ları aynı build oturumunda, mümkün olduğunca paralel çalıştırır ve ortak adımları bir kez yapar. Şu an tek bir uygulamamız var; 7. yazıda test stage'i, 8. yazıda Bookstore.Api, 10. yazıda bookstore-web bu dosyaya target olarak eklenecek ve hepsi tek docker buildx bake ile derlenecek. Docker Compose da Aralık 2025'te çıkan v5'ten beri kendi builder'ını bıraktı ve compose.yaml'daki build'leri Bake'e devrediyor; Docker Compose serisinde bu yüzden aynı mantığı göreceksin.
tools-release target'ı image'ı registry'ye gönderirken build cache'ini de ayrı bir tag'e (buildcache) export ediyor ve bir sonraki build'de oradan okuyor. mode=max yalnız final image'ın değil ara adımların da cache'ini yazar. Lokal registry ile çalıştırdığımızda çıktının sonunda exporting cache to registry ve writing cache image manifest satırları görünüyor. Cache'in builder'ın diskinde değil registry'de durması, her job'da boş açılan CI runner'larının önceki build'in cache'ini kullanabilmesini sağlar. GitHub Actions'ta bunun için ayrıca type=gha backend'i var; ikisinin pipeline'da kurulumunu GitHub Actions serisinin 11. ve 15. yazıları anlatıyor, burada kavramla yetiniyoruz. Cache mount'ların içeriği bu export'a girmez; export yalnız layer cache'ini taşır.
Üretimde Dikkat
Build süresini ölç ve hangi adımın tekrarlandığına bak. --progress=plain çıktısında CACHED olmayan her adım bir soru: bu adımın girdisi gerçekten değişti mi, yoksa sıra mı yanlış? Pipeline'da build süresinin aniden artması çoğu zaman bir COPY'nin yukarı taşındığını ya da .dockerignore'dan bir satırın silindiğini gösterir.
Restore'u etkileyen her dosyayı restore'dan önce kopyala. Central Package Management kullanıyorsan Directory.Packages.props, ortak ayarlar için Directory.Build.props, private feed için nuget.config bu gruba girer. Unutulan bir dosya ya restore'u kırar ya da (daha kötüsü) restore farklı ayarlarla çalışır ve hata ancak build'de çıkar.
Secret'lar build'e yalnız --secret ile girsin ve CI'da kısa ömürlü token kullan. Secret mount değeri image'a yazmaz ama build sırasında çalışan komut onu bir dosyaya yazarsa (ör. token'lı bir nuget.config üretip silmeyi unutmak) o dosya layer'a girer. Mount'un garantisi, senin komutunun yaptıklarını kapsamaz.
Base image güncellemesi cache'i bilerek kırar. FROM mcr.microsoft.com/dotnet/sdk:10.0 yeni bir patch'e geçtiğinde ilk adımın girdisi değişir ve her şey yeniden build edilir; bu bir hata değil, güvenlik yamalarını almanın yolu. 4. yazıdaki digest pin ve otomatik güncelleme ile ne zaman olacağına sen karar verirsin.
Sık Düşülen Tuzaklar
COPY . .'u restore'dan önce yazmak. Repository'deki her dosya (README dahil) restore'un girdisi olur ve en küçük değişiklik bütün paketleri yeniden indirir. Önce proje dosyaları, sonra kaynak.
Token'ı içeren bir dosyayı kopyalayıp sonraki RUN'da silmek. 5. yazıda gördüğümüz gibi silme yeni bir layer'a yalnız "silindi" işareti yazar; dosya alttaki layer'da durur ve image'ı çeken herkes onu çıkarabilir. ARG ve ENV ise değeri image'ın geçmişine ve config'ine yazar.
docker-container builder'da localhost'u build makinesi sanmak. Aynı bake dosyasını --builder dockerseries-builder ile çalıştırdığımızda registry'ye bağlanamadı:
docker buildx bake --builder dockerseries-builder tools-release
#18 ERROR: failed to push localhost:5000/bookstore/tools:1.0.0: failed to do request: Head "http://localhost:5000/v2/bookstore/tools/blobs/sha256:fd973353fd0802d2048e883b91ec124962890132587b3260208dd5f45665350b": dial tcp [::1]:5000: connect: connection refused
Bu builder'da BuildKit kendi container'ında çalışıyor ve localhost o container'ın kendisi. default builder engine'in içinde olduğu için aynı adres çalışıyor. Lokal registry'yle deneme yaparken bunu bil; gerçek bir registry adıyla sorun olmaz.
Cache mount'u kalıcı depolama sanmak. BuildKit'in disk temizliği (garbage collection) ya da builder'ın silinmesi cache mount'u da götürür; build bunun boş gelmesine her zaman hazır olmalı.
.NET SDK'yı QEMU ile başka mimari için çalıştırmak. Küçük bir projede bazen çalışır gibi görünür, sonra rastgele çöker ya da dakikalar sürer. Cross-compilation ya da native runner kullan.
Küçük Bir Deneme
★☆☆ Bookstore.Tools.csproj'a bir <Description> satırı ekleyip build et. Hangi adımlar CACHED kaldı, hangileri yeniden çalıştı? Aynı satırı Directory.Build.props'a yazsaydın ne olurdu?
★★☆ labs/06-private-feed'deki Dockerfile'da secret'ı env= yerine dosya olarak bağla (target=/run/secrets/nuget_token) ve dotnet restore'dan önce değişkeni NUGET_TOKEN=$(cat /run/secrets/nuget_token) ile ayarla. docker history'de bir fark görüyor musun?
★★★ docker-bake.hcl'e TAG'i git commit SHA'sından (commit'in hash değeri) üreten bir değişken ve tools-release'e ikinci bir tag ekle (4. yazıdaki semver + SHA stratejisi). docker buildx bake --print tools-release ile sonucu kontrol et.
Build artık hızlı, secret'lar image'a girmiyor, ama image hâlâ SDK'yı, kaynak kodu ve build ara dosyalarını taşıyan 1 GB'ın üzerinde bir paket. Bir sonraki yazıda Dockerfile'ı stage'lere bölüyoruz: build, test ve final ayrı target'lar olacak, final image yalnız runtime'ı ve publish çıktısını taşıyacak ve --platform=$BUILDPLATFORM ile .NET'i QEMU'suz çok mimarili build edeceğiz.