← blog · 23 Ekim 2026

Konteyner İmajı Derlemesinde Önbellek: Katman Sırası, BuildKit Önbellek Bağlamaları ve CI'da Önbelleği Taşımak

Yerelde otuz saniye süren derleme CI'da neden her seferinde dakikalar sürer? Önbelleğin nasıl karar verdiği, Dockerfile'ı ona göre dizmek, önbellek ve gizli bağlamaları, satır içi, kayıt defteri ve CI önbelleği arasında seçim ve dışa aktarılmayan önbellek tuzağı.

Bir konteyner imajının derlemesi geliştiricinin makinesinde otuz saniye sürer, CI'da ise her seferinde altı dakika. Sebep çoğu zaman aynıdır: yerelde katman önbelleği vardır, CI'daki geçici çalıştırıcı ise her işi boş bir diskle başlatır. İkinci yaygın sebep de Dockerfile'ın önbelleği daha ilk satırlarda kıracak şekilde yazılmış olmasıdır. Bu yazıda önbelleğin nasıl çalıştığını, Dockerfile'ı ona göre nasıl dizeceğinizi, BuildKit'in önbellek bağlamalarını ve önbelleği çalıştırıcılar arasında nasıl taşıyacağınızı anlatıyoruz.

Önbellek nasıl karar verir

BuildKit her talimat için bir anahtar hesaplar. Anahtar, önceki adımın sonucuna ve talimatın kendisine bağlıdır. RUN için komut metni değişmediyse adım önbellekten gelir; komutun dışarıdan ne indirdiğine bakılmaz. COPY ve ADD için kopyalanan dosyaların içeriği ve izinleri hesaba katılır, değiştirilme zamanı katılmaz. Bir adımın önbelleği kırıldığında ondan sonraki bütün adımlar da yeniden çalışır.

Buradan iki pratik sonuç çıkar. Birincisi, sık değişen şeyler dosyanın sonuna, seyrek değişenler başına yazılır. İkincisi, RUN apt-get update tek başına bir satırsa ve komut metni hiç değişmiyorsa, aylar önceki paket listesi önbellekte kalır ve bir sonraki satırdaki kurulum eski sürümleri arar.

Katman sırası: bağımlılık dosyası önce, kaynak kod sonra

En çok kazandıran değişiklik budur. Kaynak kodun tamamını kopyalayıp sonra bağımlılık kurarsanız, tek satırlık bir değişiklik bütün bağımlılıkların yeniden indirilmesine yol açar.

# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

Kilit dosyası değişmedikçe npm ci adımı önbellekten gelir. Aynı mantık Python'da requirements.txt ya da poetry.lock, Go'da go.mod ve go.sum için geçerlidir. Çok aşamalı derleme burada ikinci bir iş görür: derleme araçları ve geliştirme bağımlılıkları son imaja girmez, son aşamanın katmanları da daha az sebeple değişir.

.dockerignore bu düzenin ayrılmaz parçasıdır. COPY . . satırı .git dizinini, yerel node_modules'u, test çıktılarını ya da editör dosyalarını da kopyalıyorsa, her commit içerik değiştirdiği için önbellek kırılır. Derleme bağlamına girmesi gerekmeyen her şeyi buraya yazın.

Önbellek bağlaması: katman önbelleğinin yetmediği yer

Katman önbelleği ya hep ya hiç çalışır. Kilit dosyasına tek bir paket eklendiğinde npm ci adımı baştan çalışır ve yüzlerce paketi yeniden indirir. RUN --mount=type=cache bu boşluğu doldurur: belirtilen dizin, derlemeler arasında korunan bir önbellek dizini olarak bağlanır, imaja ise hiç girmez. Paket yöneticisi zaten indirilmiş olanları oradan alır.

Yaygın hedefler şunlardır: npm için /root/.npm, pip için /root/.cache/pip, Go için /root/.cache/go-build ve /go/pkg/mod, Maven için /root/.m2. Konteyner içinde root dışı bir kullanıcıyla derliyorsanız yol o kullanıcının ev dizinine göre değişir; yanlış yol yazmak hata vermez, sadece hiçbir şey kazandırmaz.

Debian tabanlı resmî imajlarda apt için bir tuzak var. Bu imajlar indirilen paketleri her kurulumdan sonra silen bir yapılandırma dosyasıyla gelir, yani önbellek dizinini bağlasanız da içi boş kalır. Docker'ın belgelerindeki kalıp bu dosyayı kaldırmaktır:

RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
    echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y --no-install-recommends ca-certificates curl

sharing=locked aynı anda koşan iki derlemenin apt'nin kilidi için yarışmasını engeller. update ile install'ın aynı RUN satırında olması da yukarıda anlatılan eski paket listesi sorununu çözer.

Sırlar önbelleğe ve imaja sızmasın

Özel bir paket deposuna erişim için jetonu ARG ile geçirmek yaygın bir hatadır. ARG değeri imaj geçmişinde görünebilir ve değeri her değiştiğinde önbellek kırılır. Doğru yol gizli bağlamadır:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t app:dev .

Dosya yalnızca o RUN süresince bağlanır, katmana yazılmaz ve önbellek anahtarına girmez. Jeton döndürüldüğünde derleme yine önbellekten gelir.

ARG ile ilgili ikinci tuzak sürüm bilgisidir. Commit kimliğini ARG olarak dosyanın başında tanımlayıp etiket ya da ortam değişkeni olarak kullanırsanız, her commit'te değeri değiştiği için kullanıldığı ilk adımdan sonrası önbellekten gelmez. Bu tür değerleri son aşamanın en sonuna, yalnızca gerçekten gereken yere yazın.

CI'da önbelleği taşımak

Geçici çalıştırıcıda yerel önbellek yoktur; önbelleği derlemeden sonra bir yere yazıp sonraki derlemede okumanız gerekir. BuildKit bunun için birkaç arka uç sunar. Üç yaklaşım öne çıkar.

Satır içi önbellek (type=inline) önbellek bilgisini imajın kendisine gömer. Kurması en kolay olanıdır ama yalnızca son imajın katmanlarını taşır; çok aşamalı bir derlemede derleme aşamasının katmanları kaybolur. Asıl kazancın derleme aşamasında olduğu düşünülürse bu ciddi bir eksik.

Kayıt defteri önbelleği (type=registry) önbelleği ayrı bir etiket olarak imaj kayıt defterine yazar. mode=max ile ara aşamaların katmanları da dahil edilir. Varsayılan mode=min yalnızca son imajın katmanlarını yazar.

CI sağlayıcısının önbelleği (GitHub Actions için type=gha gibi) ek bir kayıt defteri gerektirmez, ama sağlayıcının önbellek kotası ve tahliye kurallarına tabidir ve başka bir CI'a taşınmaz.

Tercihimiz kayıt defteri önbelleği, mode=max ile. İmajı zaten bir kayıt defterine itiyorsunuz, önbellek aynı kimlik doğrulamayla aynı yerde durur, CI sağlayıcısından bağımsızdır ve geliştiriciler de aynı önbelleği okuyabilir:

docker buildx create --name ci --driver docker-container --use
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/app:cache-main \
  --cache-from type=registry,ref=registry.example.com/app:cache-$BRANCH \
  --cache-to type=registry,ref=registry.example.com/app:cache-$BRANCH,mode=max \
  -t registry.example.com/app:$GIT_SHA --push .

İlk satır önemlidir: varsayılan docker sürücüsü dış önbellek türlerinin çoğunu dışa aktaramaz, bu yüzden docker-container sürücüsüyle bir builder açılır. Birden fazla --cache-from verilebilir; dal kendi önbelleğini henüz üretmemişse ana dalınkinden yararlanır. Dal başına ayrı yazma etiketi, paralel dalların birbirinin önbelleğini sürekli ezmesini önler.

Tuzaklar

Önbellek bağlamaları dışa aktarılmaz. --cache-to katman önbelleğini taşır, --mount=type=cache dizinlerinin içeriğini taşımaz. Geçici çalıştırıcıda bu dizinler her işte boştur. Yani CI'da asıl kazancı katman sırası ve kayıt defteri önbelleği sağlar; önbellek bağlaması kalıcı builder'larda ve geliştirici makinelerinde parlar.

Taban imaj güncellenmez. FROM node:22 etiketi yerelde bir kez çekildiyse BuildKit onu tekrar sormayabilir. Güvenlik yamalarını almak için düzenli derlemelerde --pull kullanın ya da taban imajı özetiyle (digest) sabitleyip güncellemeyi bir bağımlılık güncelleme aracına bırakın.

Önbellek eskimiş paket taşır. Önbellekten gelen bir apt-get install adımı haftalar önceki paketleri içerir. Haftada bir --no-cache ile tam derleme yapan zamanlanmış bir iş bunu çözer.

Kayıt defteri önbelleği büyür. mode=max ara aşamaların hepsini yazar. Dal önbelleklerini dal silindiğinde temizleyen bir saklama kuralı olmadan kayıt defteri depolaması sessizce şişer.

Önbellek isabetini ölçün. Derleme çıktısında CACHED yazan adımları sayın. Bağımlılık kurulum adımı her derlemede yeniden çalışıyorsa, sebebi neredeyse her zaman ondan önce gelen bir COPY satırı ya da değişen bir ARG değeridir.

Ne zaman bu kadarına gerek yok

Derleme zaten bir dakikanın altındaysa ve günde birkaç kez çalışıyorsa, kayıt defteri önbelleği kurmak getirdiğinden fazla bakım ister; katman sırasını düzeltmek ve .dockerignore yazmak yeterlidir. Kalıcı, tek bir builder kullanıyorsanız yerel önbellek zaten oradadır ve dışa aktarmaya gerek yoktur. Yayın imajlarında en baştan temiz derleme şartı koyan bir uyum gereksiniminiz varsa, önbelleği geliştirme ve doğrulama derlemeleriyle sınırlayın, yayın derlemesini --no-cache ile yapın ve derlenen imajı özetiyle terfi ettirin.