← Kursa Dön
📄 Text · 30 min

Layer Caching Stratejileri — Build Süresini Düşürme

Docker image'ları katmanlardan (layer) oluşur. Her RUN, COPY, ADD komutu yeni bir katman ekler. Ve Docker çok akıllı bir cache mekanizmasına sahip: eğer bir katman ve önceki tüm katmanlar değişmediyse, o katman yeniden build edilmez — cache'ten gelir. Bu, 10 dakikalık build'i 30 saniyeye düşürebilir.

Bir duvar örüyorsun diyelim. En alttaki tuğlayı değiştirirsen, üstündeki tüm tuğlaları da yeniden dizmen gerekir — çünkü alttaki değişti, artık üstündeki katmanlar da "güvenilmez." Ama en üstteki tuğlayı değiştirirsen sadece o tuğlayı değiştirirsin, alttakiler olduğu gibi kalır. Docker'ın layer cache'i tam böyle çalışır.


Layer Cache Nasıl Çalışır?

Docker bir Dockerfile'ı build ederken yukarıdan aşağıya ilerler. Her komut bir katman oluşturur. Eğer bir katman değiştiyse, o katmandan itibaren tüm sonraki katmanlar da yeniden build edilir — "cache invalidation" denen bu mekanizma zincir şeklinde çalışır.

Bunu somutlaştıralım:

FROM node:20-alpine          # Layer 1: Base image
WORKDIR /app                 # Layer 2: Çalışma dizini
COPY package.json ./         # Layer 3: package.json
RUN npm install              # Layer 4: Dependencies yükleme (uzun sürer!)
COPY . .                     # Layer 5: Kaynak kodu
RUN npm run build            # Layer 6: Build (kısa sürer)
CMD ["node", "dist/server.js"]

Şimdi src/server.ts dosyasını değiştirip tekrar build ettiğini düşün. Ne olur?

Layer 1: Base image         → ✅ Cache (değişmedi)
Layer 2: WORKDIR            → ✅ Cache (değişmedi)
Layer 3: COPY package.json  → ✅ Cache (package.json değişmedi!)
Layer 4: RUN npm install    → ✅ Cache (package.json değişmediği için!)
Layer 5: COPY . .           → ❌ MISS (src/ değişti)
Layer 6: RUN npm run build  → ❌ MISS (önceki katman değişti)

npm install cache'ten geldi! 2-5 dakika kazandık. Sadece son iki katman yeniden build edildi — birkaç saniye sürdü.

Ama şimdi aynı Dockerfile'ı kötü yazılmış haliyle karşılaştıralım:

# ❌ KÖTÜ — her kod değişikliğinde npm install tekrar çalışır
FROM node:20-alpine
WORKDIR /app
COPY . .                     # Layer 3: HER ŞEYİ kopyala
RUN npm install              # Layer 4: Dependencies
RUN npm run build            # Layer 5: Build
CMD ["node", "dist/server.js"]

src/server.ts değiştiğinde:

Layer 1: Base image         → ✅ Cache
Layer 2: WORKDIR            → ✅ Cache
Layer 3: COPY . .           → ❌ MISS (src/ değişti → tüm dizin değişti)
Layer 4: RUN npm install    → ❌ MISS (önceki katman değişti!)
Layer 5: RUN npm run build  → ❌ MISS

npm install cache'i patladı! Her kod değişikliğinde 2-5 dakika bekleyeceksin. Fark sadece COPY komutlarının sıralaması.


Altın Kural: Az Değişen → Önce, Çok Değişen → Sonra

Bu kuralı her Dockerfile'da uygula. Değişme sıklığına göre düzenle:

En az değişen → İlk sıraya
─────────────────────────
Base image           (ayda bir)
Sistem paketleri     (haftada bir)
Dependency dosyaları (günde bir)
Dependencies install (günde bir)
Kaynak kodu          (her commit'te)
Build komutu         (her commit'te)
─────────────────────────
En çok değişen → Son sıraya

Node.js — Doğru Sıralama

FROM node:20-alpine
WORKDIR /app

# 1. Dependency dosyalarını kopyala (nadiren değişir)
COPY package.json package-lock.json ./

# 2. Dependencies yükle (package.json değişmediyse cache'ten gelir)
RUN npm ci --only=production

# 3. Kaynak kodunu kopyala (sık değişir)
COPY . .

# 4. Build
RUN npm run build
CMD ["node", "dist/server.js"]

Python — Doğru Sıralama

FROM python:3.12-slim
WORKDIR /app

# 1. Requirements (nadiren değişir)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 2. Kaynak kodu (sık değişir)
COPY . .
CMD ["python", "app.py"]

Go — Doğru Sıralama

FROM golang:1.22-alpine
WORKDIR /app

# 1. Module dosyaları (nadiren değişir)
COPY go.mod go.sum ./
RUN go mod download

# 2. Kaynak kodu (sık değişir)
COPY . .
RUN go build -o /server .

Java — Doğru Sıralama

FROM maven:3.9-eclipse-temurin-21
WORKDIR /app

# 1. POM (nadiren değişir)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 2. Kaynak kodu (sık değişir)
COPY src ./src
RUN mvn package -DskipTests -B

Pattern'i görüyor musun? Dil farketmez — her zaman önce dependency tanım dosyasını kopyala, sonra install et, sonra kaynak kodunu kopyala.


RUN Komutlarını Birleştir

Her RUN komutu bir layer oluşturur. Gereksiz layer'lar image boyutunu artırır ve cache verimliliğini düşürür:

# ❌ KÖTÜ — 3 ayrı layer, 3x metadata overhead
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*
# ✅ İYİ — tek layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl wget && \
    rm -rf /var/lib/apt/lists/*

Özellikle apt-get update ve apt-get install mutlaka aynı RUN'da olmalı. Ayrı olurlarsa, install komutu cache'teki eski package listesini kullanabilir ve paket bulunamaz hatası verir.

Ama aşırıya da kaçma:

# ❌ AŞIRI — her şeyi tek satırda yapmak cache verimliliğini düşürür
RUN apt-get update && apt-get install -y git && \
    npm ci && npm run build && npm prune --production && \
    rm -rf /var/lib/apt/lists/*

Mantıksal gruplar halinde birleştir: sistem paketleri bir RUN, app dependencies bir RUN, build bir RUN.


BuildKit Cache Mount

Docker BuildKit'in en güçlü özelliklerinden biri --mount=type=cache. Bu, build sırasında kalıcı cache dizinleri oluşturur — paket yöneticisinin indirme cache'i build'ler arası korunur:

# syntax=docker/dockerfile:1

# Node.js
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# Python
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Go
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /server .

# Maven
RUN --mount=type=cache,target=/root/.m2/repository \
    mvn package -DskipTests -B

# Gradle
RUN --mount=type=cache,target=/root/.gradle \
    ./gradlew bootJar --no-daemon

# apt
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y curl

Normal cache'te package.json değiştiğinde npm install tüm paketleri yeniden indirir. Cache mount ile sadece değişen paketler indirilir — geri kalanı mount edilen cache'ten gelir. Bu, 100+ dependency'li projelerde build süresini %50-80 azaltabilir.

⚠️ Önemli: Cache mount'lar CI/CD ortamında varsayılan olarak boştur (her build temiz başlar). CI pipeline'ının cache'i desteklediğinden emin ol — GitHub Actions'da actions/cache, GitLab CI'da cache: key'i.


.dockerignore — Cache Katili Dosyaları Engelle

.dockerignore dosyası, build context'e dahil edilmeyecek dosyaları belirtir. Bu iki nedenle kritik:

  1. Build hızı: Gereksiz dosyalar build context olarak Docker daemon'a gönderilmez

  2. Cache koruması: node_modules, .git gibi sürekli değişen dizinler COPY cache'ini patlatmaz

# .dockerignore
node_modules
.git
.gitignore
*.md
dist
build
.env
.env.*
.vscode
.idea
docker-compose*.yml
Dockerfile*
coverage
__pycache__
*.pyc
.pytest_cache
target

.dockerignore olmadan ne olur? COPY . . komutu .git dizinini de kopyalar. Her commit'te .git değişir → COPY katmanı invalidate olur → sonraki tüm katmanlar yeniden build edilir. Sadece .git'i ignore etmek bile cache hit oranını dramatik artırır.

Test et

# Build context boyutunu gör
docker build --no-cache . 2>&1 | head -5
# Sending build context to Docker daemon  450MB  ← KÖTÜ!

# .dockerignore ekledikten sonra
docker build --no-cache . 2>&1 | head -5
# Sending build context to Docker daemon  2.3MB  ← İYİ!

Conditional COPY ve Spesifik Dosya Kopyalama

# ❌ KÖTÜ — tüm dizini kopyalar, herhangi bir dosya değişse cache patlar
COPY . .

# ✅ İYİ — sadece gerekli dizinleri kopyala
COPY src/ ./src/
COPY config/ ./config/
COPY public/ ./public/

Daha spesifik COPY komutları, cache invalidation'ı minimize eder. README değiştiğinde src/'in cache'i etkilenmez.


Cache Debugging — Neden Cache Miss?

Build süresinin neden uzun olduğunu anlamak için:

# BuildKit progress ile build et
DOCKER_BUILDKIT=1 docker build --progress=plain .

Çıktıda her adımın cache durumunu görebilirsin:

#7 [3/6] COPY package.json ./
#7 CACHED

#8 [4/6] RUN npm ci
#8 CACHED

#9 [5/6] COPY . .
#9 sha256:abc123...
#9 DONE 0.3s

#10 [6/6] RUN npm run build
#10 DONE 12.4s

"CACHED" → cache'ten geldi, "DONE X.Xs" → yeniden build edildi.

Build istatistikleri

docker buildx build --progress=plain . 2>&1 | grep -E "CACHED|DONE"

CI/CD'de Cache Stratejileri

CI/CD pipeline'larında her build genelde temiz bir ortamda başlar — cache boştur. Bu sorunu çözmenin birkaç yolu var:

GitHub Actions Cache

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

type=gha GitHub Actions'ın cache storage'ını kullanır. mode=max tüm ara katmanları cache'ler — sadece final image'ı değil.

Registry Cache

# Build ve cache'i registry'ye push et
docker buildx build \
    --cache-from type=registry,ref=myregistry/myapp:cache \
    --cache-to type=registry,ref=myregistry/myapp:cache,mode=max \
    -t myregistry/myapp:latest \
    --push .

Cache registry'de durur — her CI runner bu cache'i kullanabilir.

Inline Cache

# En basit yöntem — image'ın kendisine cache metadata'sı göm
docker buildx build \
    --cache-from myregistry/myapp:latest \
    --build-arg BUILDKIT_INLINE_CACHE=1 \
    -t myregistry/myapp:latest .

Yeni build, önceki push'lanmış image'ı cache kaynağı olarak kullanır.


Cache ile İlgili Yaygın Hatalar

Hata 1 — Timestamp'li komutlar cache'i her zaman patlatır:

# ❌ Her build'de farklı sonuç → cache asla çalışmaz
RUN echo "Build: $(date)" > /app/version.txt
# ✅ Build argument kullan — aynı ARG → cache çalışır
ARG BUILD_DATE
RUN echo "Build: ${BUILD_DATE}" > /app/version.txt

Hata 2 — ADD ile URL veya tar:

# ❌ ADD, URL'den indirirken cache davranışı tutarsız
ADD https://example.com/data.tar.gz /app/

# ✅ COPY + RUN ile daha kontrollü
COPY data.tar.gz /app/
RUN tar xzf /app/data.tar.gz

ADD komutu tar dosyalarını otomatik açar ve URL'lerden indirir. Ama cache davranışı COPY'den farklı ve daha az tahmin edilebilir. Basit dosya kopyalama için her zaman COPY kullan.

Hata 3 — Platform değişkeni cache'i ayırır:

# Bu iki komut farklı cache kullanır!
docker build .                              # linux/amd64
docker build --platform linux/arm64 .       # linux/arm64

Bu Derste Ne Öğrendik?

  • Docker layer cache yukarıdan aşağıya zincir şeklinde çalışır — bir katman değişirse altındakiler de invalidate olur.

  • Altın kural: Az değişen dosyaları önce, çok değişeni sonra kopyala.

  • Dependency dosyalarını önce kopyala (package.json, requirements.txt, go.mod) — dependencies cache'ten gelir.

  • RUN komutlarını mantıksal gruplar halinde birleştir — ama aşırıya kaçma.

  • BuildKit cache mount ile paket indirme cache'ini build'ler arası koru.

  • .dockerignore ile gereksiz dosyaları build context'ten çıkar — cache koruması + hız.

  • CI/CD'de registry cache veya GHA cache kullan — her build'de temiz başlama.

  • --progress=plain ile cache hit/miss durumunu debug et.

Son derse hazır mısın? Image boyut optimizasyonunun son detaylarını — base image seçimi, distroless, Alpine sorunları ve security scanning — inceleyeceğiz.