DOCKER IMAGE LAYERING VE CACHE
Çoğu geliştirici Docker image'ini tek parça bir paket gibi düşünür: build edersin, push edersin, başka bir makinede pull edip çalıştırırsın. Pratikte bu yanıltıcı bir basitleştirmedir. Bir Docker image gerçekte birbiri üzerine yığılmış, her biri salt-okunur (read-only) katmanların bir koleksiyonudur — ve bu yığın yapısı Docker'ın hem build hızının hem disk verimliliğinin temelidir. Layering mantığını anlamadan yazılan bir Dockerfile, build süresini 30 saniyeden 8 dakikaya çıkarabilir; aynı uygulamanın image boyutunu 80 MB yerine 1.4 GB olarak şişirebilir. Bu yazı geliştirici, DevOps mühendisi veya sistem yöneticisi olarak Docker ile çalışan herkesin image iç yapısı ve build cache mekanizması hakkında net bir zihin haritası kurması için yazıldı.
Image Bir Paket Değil, Katmanlar Yığını
Bir docker pull nginx:alpine komutu çalıştırdığınızda ekrana düşen birden çok satıra dikkat ettiniz mi? Her satır ayrı bir hash ile başlar ve "Pull complete" ile biter. Bu satırların her biri ayrı bir layer'dır. Image, bu layer'ların belirli bir sırayla üst üste konulmuş halidir.
Her Dockerfile direktifi (FROM, RUN, COPY, ADD gibi) yeni bir katman üretir. ENV, LABEL, WORKDIR gibi metadata direktifleri de teorik olarak katman üretir ancak boyutları sıfıra yakındır. Önemli olan: katmanlar değişmez. Bir layer oluştuktan sonra üzerinde değişiklik yapamazsınız; yalnızca onun üstüne yeni bir layer ekleyebilirsiniz.
Bunu somutlaştırmak için basit bir örnek:
- Layer 1:
FROM ubuntu:22.04— Ubuntu base image (~77 MB) - Layer 2:
RUN apt-get update && apt-get install -y python3— Python kurulumu (~45 MB delta) - Layer 3:
COPY requirements.txt /app/— tek dosya (~2 KB) - Layer 4:
RUN pip install -r /app/requirements.txt— bağımlılıklar (~120 MB) - Layer 5:
COPY . /app/— uygulama kodu (~5 MB)
Final image yaklaşık 247 MB'dir, ama disk üzerinde 5 ayrı katman olarak tutulur. Aynı base'i kullanan başka bir image yarattığınızda Layer 1 yeniden indirilmez veya kopyalanmaz; Docker onu paylaşır.
Union Filesystem ve Katmanların Birleşmesi
Katmanlar disk üzerinde ayrı klasörler olarak durur. Container çalıştığında Docker, bu katmanları union filesystem (genellikle overlay2) kullanarak tek bir dosya hiyerarşisinde birleştirir. Bir üst katman alt katmanın dosyalarını "üzerine yazıyor" gibi gözükür ama gerçekte alt katman değişmez — sadece üst katmanda aynı yola farklı bir dosya konur. Container açıldığında üstte bir writable layer daha eklenir; runtime'da yapılan tüm değişiklikler bu katmana yazılır.
Bu yapı Docker'ın iki süper gücünü doğurur. Birincisi: aynı base image'i kullanan 50 container çalışırken disk üzerinde tek bir Ubuntu kopyası yeterlidir. İkincisi: aynı katmanı kullanan iki farklı image'ın o katmanı yeniden indirmesine gerek yoktur. Container ekosisteminin ve Docker'ın genel teknik mimarisi üzerine yapılan açıklamalar, bu paylaşım modelinin neden bu kadar verimli olduğunu daha geniş bir bağlama oturtur.

Build Cache Neyi Tutar?
Docker build sırasında her direktifi çalıştırmadan önce şu soruyu sorar: "Bu komutu daha önce aynı girdilerle çalıştırdım mı?" Cevap evet ise direktifi yeniden çalıştırmaz; önceden ürettiği katmanı doğrudan kullanır. Bu mekanizmaya build cache denir ve Docker'ın hız avantajının kalbidir.
Cache anahtarı iki şeyden oluşur: direktifin kendisi ve girdileri. RUN apt-get install -y curl komutu için anahtar komutun metnidir. COPY package.json /app/ komutu için anahtar komut metni artı package.json dosyasının içeriği hash'idir. Dosya tek byte değişirse hash değişir, cache miss olur, layer baştan üretilir.
Cache Ne Zaman Geçersiz Olur?
Cache invalidasyonu kuralları net ve katıdır. Bir direktif cache'i kaybederse, ondan sonra gelen tüm direktiflerin cache'i de düşer. Bu, Dockerfile sıralamasının neden hayati olduğunu açıklar.
Tetikleyiciler:
- Direktif metni değişti:
RUN apt-get install curlyerineRUN apt-get install curl wgetyazdınız. Hash değişti, cache miss. - COPY/ADD edilen dosyanın içeriği değişti: Dosya boyutu, izinleri veya içeriği farklı ise hash farklı olur.
- Üstteki bir layer invalidate oldu: Bir önceki adım yeniden üretilmek zorunda kaldıysa bu adımın bağlamı değişmiş demektir; cache yok sayılır.
--no-cacheflag'i: Build komutunda manuel olarak cache'i devre dışı bıraktınız.- Build context değişti (ADD ile uzak kaynak): Uzak URL'den çekilen içerik değiştiyse cache düşer.
En sık karşılaşılan hata: uygulama kodunu Dockerfile'ın üst tarafına COPY etmek. Her commit'te kod değişeceği için bu adımdan sonraki her şey yeniden üretilir. pip install veya npm install gibi pahalı adımlar kod kopyalamadan önce yapılmalıdır.
Dockerfile Sıralaması: Pratik Optimizasyon
İyi bir Dockerfile, değişme sıklığına göre yukarıdan aşağı sıralanır: en az değişen üstte, en sık değişen altta. Tipik bir Node.js uygulaması için yanlış ve doğru sıralama:
Yanlış:
FROM node:20-alpineWORKDIR /appCOPY . .RUN npm installCMD ["node", "server.js"]
Bu Dockerfile her kod değişikliğinde npm install'u yeniden çalıştırır. 800 paketli bir projede tek satır değişiklik 90 saniyelik bağımlılık kurulumunu tetikler.
Doğru:
FROM node:20-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .CMD ["node", "server.js"]
Burada package.json ve package-lock.json önce kopyalanır, bağımlılıklar kurulur, sonra kod kopyalanır. package.json değişmediği sürece npm ci cache'ten gelir. Pratik fark: her commit'te 90 saniye yerine 3-4 saniye.
Multi-stage Build ile Image Küçültme
Layer mantığı image boyutunu doğrudan etkiler. Build sırasında ihtiyaç duyulan ama runtime'da gereksiz olan araçlar (derleyiciler, build script'leri, dev dependency'ler) image'a kalırsa boyut şişer. Multi-stage build bu sorunu çözer: birden çok FROM direktifi kullanarak ara aşamalarda build yapılır, final aşamaya yalnızca üretilen artefaktlar taşınır.
Go uygulaması için klasik örnek:
FROM golang:1.22-alpine AS builder— build aşamasıWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN go build -o /app/serverFROM alpine:3.19— runtime aşamasıCOPY --from=builder /app/server /app/serverCMD ["/app/server"]
Builder aşaması yaklaşık 350 MB'dir; içinde Go derleyici, kaynak kod, modül cache her şey vardır. Final image ise yalnızca derlenmiş binary'yi içerir — toplam boyut ~15 MB'a kadar düşer. Disk, network ve cold-start süresi üzerinde ciddi kazanç sağlar. Bu pratiklerin yapılandırılmış şekilde ele alındığı Docker eğitimi programını incelemek isteyenler için faydalı olabilir.
RUN Birleştirme ve Layer Sayısı Tradeoff'u
Her RUN direktifi yeni katman üretir. Yan etki: ara dosyalar (apt cache, /tmp dosyaları, indirilen tar'lar) bir RUN komutunun sonunda silinse bile, önceki katmanda kaldıkları sürece image boyutuna eklenirler. Bu yüzden bağlantılı komutlar && ile zincirlenir ve cleanup aynı RUN içinde yapılır:
Şişen versiyon:
RUN apt-get updateRUN apt-get install -y curl wget gitRUN rm -rf /var/lib/apt/lists/*
Üçüncü RUN silme yapar ama önceki iki katmanda apt cache hâlâ duruyor. Image küçülmez.
Tek katman:
RUN apt-get update && apt-get install -y curl wget git && rm -rf /var/lib/apt/lists/*
Tek katmanda kuruyor ve aynı katmanda temizliyor. Sonuç image kayda değer derecede daha küçük.
BuildKit ve Yeni Cache Yetenekleri
Modern Docker (20.10+) varsayılan olarak BuildKit kullanır. BuildKit, klasik build engine'in üzerine gelişmiş özellikler ekler: paralel layer build, daha akıllı cache analizi, RUN --mount=type=cache ile direktif-içi kalıcı cache. Örneğin npm veya pip indirme cache'ini katmanın dışında tutarak hem image'ı şişirmeden hem indirilenleri yeniden kullanarak build alabilirsiniz:
RUN --mount=type=cache,target=/root/.npm npm ci
Bu cache image'a dahil edilmez ama bir sonraki build'de doluyken yeniden kullanılır. Özellikle CI/CD ortamlarında klasik COPY bazlı cache'in yetersiz kaldığı senaryolarda kurtarıcıdır.
Sık Karşılaşılan Tuzaklar
Layer ve cache mantığını yeni öğrenen ekiplerin tipik hataları:
- Sırların layer'a sızması:
COPY .envveyaRUN echo $SECRETile sır image'a yazıldığında, ileride o layer'ı silen bir komut yazsanız bile sır önceki katmanda kalır. Image push edildiğinde sızar. Sırlar için BuildKit secret mount kullanın. :latesttag bağımlılığı: Base olarakFROM node:latestkullanmak cache'i belirsizleştirir. Reprodüksiyonu için sürüm pinleyin (node:20.11.1-alpine3.19).- .dockerignore eksikliği:
COPY . .ile node_modules, .git, log dosyaları image'a girer; hem boyut artar hem cache her şey değiştiğinde düşer. - Çok fazla küçük RUN: Layer sayısı eskiden 127 ile sınırlıydı; modern overlay2'de daha esnek ama performans yine de zarar görür. Mantıken birleştirilebilir komutları zincirleyin.
- Cache'i fazla agresif kullanmak:
apt-get updateileapt-get install'u ayrı katmanlara koyarsanız update layer'ı eski kalır, install yeni paketleri bulamayabilir. Her zaman aynıRUNiçinde tutun.
Layer mantığı yüzeyde basittir ama disiplin gerektirir. Bir Dockerfile yazıldığı anda doğru çalışıyor olabilir; sorun büyük çoğunlukla altıncı ayda bir tek satır değişikliğin bütün build'i baştan tetiklediğinde fark edilir. Geriye dönük Dockerfile'larınızı bu gözle bir kez okumak, CI/CD süresini yarıya indirebilecek küçük kazanımlar çıkarır.



