Docker Multi-Stage Build ile Küçük ve Güvenli Image'lar
Docker Multi-Stage Build ile Küçük ve Güvenli Image'lar
Bir Spring Boot uygulamasını Docker image'ına dönüştürdün. docker images komutunu çalıştırdığında boyutu gördün: 800MB. Uygulamanın kendisi 30MB'lık bir JAR dosyası. Geri kalan 770MB ne? JDK, build araçları, Maven cache, kaynak kodlar, test dosyaları — hiçbiri production'da gerekli olmayan şeyler. Bu image'ı her deploy'da registry'ye push ediyorsun, her node'a pull ediyorsun. Bant genişliği, depolama, başlatma süresi — hepsi gereksiz yere şişiyor.
Daha kötüsü, o 770MB içinde güvenlik açıkları barınıyor. JDK'nın derleme araçları, gcc, make, wget gibi utility'ler — bir saldırgan container'a sızarsa bu araçları kullanarak daha fazla zarar verebilir. Yüzey alanı (attack surface) gereksiz yere geniş.
Multi-stage build tam olarak bu iki problemi çözer: image boyutunu dramatik şekilde küçültür ve güvenlik yüzeyini daraltır. Bu yazıda multi-stage build'i derinlemesine öğreneceksin — neden önemli olduğunu, Java/Spring Boot ve Python projeleri için nasıl uygulanacağını, layer caching stratejilerini ve güvenlik best practice'lerini.
Multi-Stage Build Nedir?
Normal bir Dockerfile'da tek bir FROM ifadesi vardır ve tüm adımlar aynı image üzerinde gerçekleşir. Multi-stage build'de birden fazla FROM ifadesi kullanırsın — her biri bir "aşama" (stage) oluşturur. Son aşama final image olur; önceki aşamalardan sadece ihtiyacın olan dosyaları kopyalarsın.
Bunu bir fabrika analojisiyle düşün. Arabanın montaj hattında kaynak makineleri, boya kabinleri, test ekipmanları var. Ama müşteriye teslim ettiğin arabada bu ekipmanlar yok — sadece bitmiş ürün var. Multi-stage build de aynı mantık: build aşamasında tüm araçları kullan, ama final image'a sadece çalıştırılabilir ürünü koy.
Tek Stage vs Multi-Stage: Farkı Görelim
Tek Stage Dockerfile (Kötü Örnek)
# ❌ Tek stage — her şey final image'da kalıyor
FROM maven:3.9-eclipse-temurin-21
WORKDIR /app
# Tüm kaynak kodu kopyala
COPY . .
# Build et
RUN mvn clean package -DskipTests
# Çalıştır
EXPOSE 8080
CMD ["java", "-jar", "target/myapp-1.0.0.jar"]Bu image'ın boyutu: ~850MB. İçinde Maven, JDK (derleme araçlarıyla birlikte), kaynak kodlar, .git klasörü, test dosyaları, indirilen tüm bağımlılıklar var. Bunların hiçbiri runtime'da gerekli değil.
Multi-Stage Dockerfile (İyi Örnek)
# ✅ Multi-stage — build ve runtime ayrı
# === AŞAMA 1: Build ===
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# Önce sadece pom.xml kopyala — bağımlılıkları cache'le
COPY pom.xml .
RUN mvn dependency:resolve
# Şimdi kaynak kodu kopyala ve build et
COPY src ./src
RUN mvn clean package -DskipTests
# === AŞAMA 2: Runtime ===
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Build aşamasından sadece JAR dosyasını al
COPY --from=builder /app/target/myapp-1.0.0.jar app.jar
# Güvenlik: root olmayan kullanıcı
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Bu image'ın boyutu: ~180MB. Fark nereden geliyor?
| Bileşen | Tek Stage | Multi-Stage |
|---|---|---|
| Base image | maven:3.9 (~400MB) | eclipse-temurin:21-jre-alpine (~100MB) |
| JDK vs JRE | Full JDK (~300MB) | Sadece JRE (~150MB) |
| Maven + cache | ~200MB | 0 (kopyalanmadı) |
| Kaynak kod | ~10MB | 0 (kopyalanmadı) |
| Toplam | ~850MB | ~180MB |
780% küçülme — ve içinde sadece uygulamayı çalıştırmak için gereken minimum bileşenler var.
Spring Boot Projesi: Production-Ready Dockerfile
Gerçek bir Spring Boot projesi için optimize edilmiş, production-ready Dockerfile:
# === AŞAMA 1: Bağımlılık Çözümleme ===
FROM maven:3.9-eclipse-temurin-21-alpine AS deps
WORKDIR /app
COPY pom.xml .
# Bağımlılıkları offline çöz — kaynak kod değişse bile bu layer cache'ten gelir
RUN mvn dependency:go-offline -B
# === AŞAMA 2: Build ===
FROM maven:3.9-eclipse-temurin-21-alpine AS builder
WORKDIR /app
# Bağımlılık cache'ini önceki aşamadan al
COPY --from=deps /root/.m2 /root/.m2
COPY pom.xml .
COPY src ./src
# Production profili ile build et, testleri atla
RUN mvn clean package -DskipTests -Pprod -B \
&& mv target/*.jar app.jar \
&& java -Djarmode=layertools -jar app.jar extract
# === AŞAMA 3: Runtime ===
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
# Spring Boot layered JAR — her katman ayrı Docker layer olur
# Değişmeyen katmanlar cache'ten gelir, deploy hızlanır
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
# Güvenlik ayarları
RUN addgroup -S spring && adduser -S spring -G spring \
&& chown -R spring:spring /app
USER spring
# JVM optimizasyonları
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
EXPOSE 8080
# Container-aware JVM ayarlarıyla başlat
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]Spring Boot Layered JAR nedir? Spring Boot 2.3+'dan itibaren JAR dosyasını katmanlara ayırabilirsin. Bağımlılıklar (dependencies) nadiren değişir, uygulama kodu sık değişir. Her katmanı ayrı Docker layer olarak kopyaladığında, sadece değişen katman yeniden build edilir. Bu, CI/CD pipeline'ında image build süresini ve push boyutunu dramatik şekilde azaltır.
Python Projesi: Multi-Stage Build
Python projeleri için de multi-stage build çok faydalı. Özellikle C extension gerektiren paketlerde (numpy, pandas, cryptography) build araçları runtime'da gereksiz:
# === AŞAMA 1: Bağımlılık Build ===
FROM python:3.12-slim AS builder
WORKDIR /app
# Build araçlarını kur (C extension'lar için gerekebilir)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Virtual environment oluştur — temiz izolasyon
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Önce requirements.txt kopyala — bağımlılık cache'lemesi
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# === AŞAMA 2: Runtime ===
FROM python:3.12-slim AS runtime
WORKDIR /app
# Sadece runtime'da gereken sistem kütüphaneleri
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Virtual environment'ı builder'dan kopyala
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Uygulama kodunu kopyala
COPY . .
# Güvenlik: root olmayan kullanıcı
RUN useradd --create-home --shell /bin/bash appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Neden virtual environment? Container içinde venv gereksiz gibi görünebilir. Ama multi-stage build'de büyük avantaj sağlar: tüm paketler /opt/venv içinde toplandığı için COPY --from=builder /opt/venv /opt/venv ile tek seferde kopyalarsın. Sistem Python'unun paketleriyle karışmaz, temiz bir izolasyon sağlar.
| Bileşen | Tek Stage | Multi-Stage |
|---|---|---|
| Base image | python:3.12 (~1GB) | python:3.12-slim (~150MB) |
| Build araçları (gcc vb.) | ~200MB | 0 |
| pip cache | ~100MB | 0 |
| Toplam (tipik) | ~1.4GB | ~200MB |
Layer Caching: Build Süresini Optimize Et
Docker her RUN, COPY, ADD komutunu bir layer (katman) olarak cache'ler. Bir layer değiştiğinde, ondan sonraki tüm layer'lar yeniden build edilir. Bu yüzden Dockerfile'daki komutların sırası kritik:
# ❌ KÖTÜ SIRA — kod değiştiğinde bağımlılıklar da yeniden indirilir
COPY . . # kaynak kod değişti → bu layer bozuldu
RUN pip install -r requirements.txt # ↑ önceki layer bozulduğu için bu da yeniden çalışır!
# ✅ İYİ SIRA — bağımlılıklar cache'ten gelir
COPY requirements.txt . # sadece requirements değişmediyse cache'ten
RUN pip install -r requirements.txt # bağımlılıklar cache'ten gelir
COPY . . # sadece bu layer yeniden build edilirAltın kural: En az değişen dosyaları en üste, en çok değişen dosyaları en alta koy. Bağımlılık dosyaları (pom.xml, requirements.txt, package.json) nadiren değişir — bunları önce kopyala ve bağımlılıkları çöz. Uygulama kodunu en son kopyala.
.dockerignore Dosyası
.gitignore gibi, .dockerignore da gereksiz dosyaların image'a girmesini engeller:
# .dockerignore
.git
.gitignore
*.md
README*
LICENSE
docker-compose*.yml
.env
.env.*
__pycache__
*.pyc
.pytest_cache
node_modules
target/
build/
.idea
.vscode
*.log.dockerignore olmadan COPY . . komutu .git klasörünü (yüzlerce MB olabilir), node_modules'ü, build çıktılarını ve hassas dosyaları (.env) image'a kopyalar. Build context'i büyür, build süresi artar ve gereksiz layer invalidation olur.
Güvenlik Best Practice'leri
1. Root Olmayan Kullanıcı Kullan
# Her zaman non-root kullanıcı oluştur ve geç
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuserContainer'lar varsayılan olarak root kullanıcıyla çalışır. Bir saldırgan container'dan kaçarsa (container escape), host'ta da root yetkilerine sahip olabilir. Non-root kullanıcı bu riski azaltır.
2. Minimal Base Image Seç
# ❌ Full Ubuntu — 200+ paket, geniş saldırı yüzeyi
FROM ubuntu:22.04
# 😐 Slim — daha az paket ama hâlâ geniş
FROM python:3.12-slim
# ✅ Alpine — minimal, ~5MB base
FROM python:3.12-alpine
# ✅✅ Distroless — shell bile yok, en güvenli
FROM gcr.io/distroless/java21-debian12Distroless image'lar Google'ın geliştirdiği, sadece uygulamayı çalıştırmak için gereken minimum bileşenleri içeren image'lardır. Shell, paket yöneticisi, utility komutları yok. Bu, saldırı yüzeyini minimum'a indirir — container'a sızan bir saldırgan sh, curl, wget bulamaz.
# Distroless ile Spring Boot — en güvenli seçenek
FROM maven:3.9-eclipse-temurin-21-alpine AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/target/*.jar /app/app.jar
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]3. Image'ı Güvenlik Tarayıcısıyla Tara
# Trivy ile güvenlik taraması — ücretsiz ve açık kaynak
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image myapp:latest
# Docker Scout ile tarama (Docker Desktop entegre)
docker scout cves myapp:latest
# Snyk ile tarama
docker scan myapp:latestHer build sonrası CI/CD pipeline'ında güvenlik taraması çalıştır. Critical ve High seviye açıkları olan image'ları deploy etme.
4. Sabit Versiyon Tag'leri Kullan
# ❌ latest tag — her build'de farklı sonuç olabilir
FROM python:latest
# ❌ Minor versiyon — patch güncellemesi break edebilir
FROM python:3.12
# ✅ Tam versiyon + variant — tekrarlanabilir build
FROM python:3.12.7-slim-bookworm
# ✅✅ SHA digest — değiştirilemez, en güvenilir
FROM python:3.12.7-slim-bookworm@sha256:abc123...latest tag'i her gün farklı bir image'a işaret edebilir. Bugün çalışan Dockerfile yarın bozulabilir. Tam versiyon kullanmak build tekrarlanabilirliğini (reproducibility) garanti eder.
5. Hassas Verileri Image'a Koymayın
# ❌ TEHLİKELİ — secret image layer'ında kalıcı olarak saklanır
COPY .env /app/.env
RUN echo "DB_PASSWORD=secret123" > /app/config
# ❌ TEHLİKELİ — ARG bile intermediate layer'da görülebilir
ARG DB_PASSWORD
RUN echo "Password: $DB_PASSWORD" > /app/config
# ✅ Runtime'da environment variable olarak geç
ENV DB_HOST=localhost
# Password'ü docker run -e ile veya docker secret ile ver
# ✅ Docker BuildKit secret mount (build zamanı için)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm installDocker image layer'ları herkes tarafından incelenebilir (docker history, dive aracı). Image'a koyduğun her şey potansiyel olarak görülebilir — silsen bile önceki layer'da kalır.
Build Boyutunu Analiz Et: dive Aracı
dive aracı ile image'ın her layer'ını inceleyebilir ve gereksiz dosyaları tespit edebilirsin:
# dive'ı kur
brew install dive # macOS
# veya
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest myapp:latestdive her layer'ın ne kadar yer kapladığını, hangi dosyaların eklendiğini/değiştirildiğini gösterir. "Wasted space" metriği ile gereksiz dosyaları tespit edebilirsin.
Yaygın Hatalar ve Tuzaklar
1. apt-get Cache'ini Temizlememek
# ❌ apt cache layer'da kalır — gereksiz 100MB+
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # Bu ayrı layer — önceki layer'daki cache hâlâ var!
# ✅ Tek RUN komutunda yap — cache layer'a yazılmaz
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*Docker layer'ları immutable'dır (değiştirilemez). Bir layer'da dosya oluşturup sonraki layer'da silsen bile, ilk layer dosyayı içermeye devam eder. Bu yüzden install ve cleanup aynı RUN komutunda olmalı.
2. Multi-Stage'de Yanlış Dosyayı Kopyalamak
# ❌ Tüm build dizinini kopyalamak — multi-stage'in amacını yok eder
COPY --from=builder /app /app
# ✅ Sadece ihtiyacın olan artifact'ı kopyala
COPY --from=builder /app/target/myapp.jar /app/app.jarCOPY --from=builder /app /app dersen kaynak kodları, build cache'ini, test dosyalarını da kopyalarsın. Multi-stage build'in tüm avantajı kaybolur.
3. Build Context'i Kontrol Etmemek
# Build süresi neden bu kadar uzun?
# .git klasörü 500MB olabilir ve COPY . . ile image'a girer
Sending build context to Docker daemon 524.3MB # ← çok büyük!.dockerignore dosyası oluşturmayı her zaman ilk adım yap. Build context boyutunu docker build çıktısının ilk satırında kontrol et.
4. Her Şeyi Alpine'a Taşımaya Çalışmak
Alpine Linux musl libc kullanır, çoğu Linux dağıtımı glibc kullanır. Bu uyumsuzluk bazı binary'lerde (özellikle Python C extension'ları) sorunlara yol açabilir:
# ❌ Alpine'da bazı Python paketleri derlenemez veya yavaş çalışır
FROM python:3.12-alpine
RUN pip install numpy pandas # Çok uzun sürer — prebuilt wheel yok, C'den derler
# ✅ slim daha iyi seçim — glibc uyumlu, makul boyut
FROM python:3.12-slim-bookworm
RUN pip install numpy pandas # Prebuilt wheel kullanır, hızlıAlpine küçük boyutuyla cazip ama Python ekosistemiyle her zaman uyumlu değil. Java (JVM) için Alpine genellikle sorunsuz çalışır; Python için slim variant'ı genellikle daha güvenli bir tercih.
Best Practices Özet
Her zaman multi-stage build kullan. Build araçları ve runtime'ı ayır. Boyut ve güvenlik farkı dramatik.
Layer caching'i optimize et. Bağımlılık dosyalarını (pom.xml, requirements.txt) kaynak koddan önce kopyala.
.dockerignoredosyasını mutlaka oluştur.Minimal base image seç.
alpineveyaslimvariant kullan. Mümkünsedistrolesstercih et.Root ile çalıştırma.
USERkomutuyla non-root kullanıcıya geç. Bu basit adım güvenliği önemli ölçüde artırır.Sabit versiyon tag'leri kullan.
latestkullanma. Tam versiyon numarası veya SHA digest ile tekrarlanabilir build garanti et.Secret'ları image'a koyma. Environment variable veya Docker secret mekanizmalarını kullan.
Image'ı tara. CI/CD pipeline'ında Trivy, Snyk veya Docker Scout ile güvenlik taraması yap.
Image boyutunu izle.
docker imagesvediveile boyutu düzenli kontrol et. Beklenmedik büyüme varsa araştır.
Sonuç
Multi-stage build, modern Docker kullanımının olmazsa olmazı. Bu yazıda öğrendiklerini özetleyelim:
Multi-stage build ile build ve runtime aşamalarını ayır — boyutu %70-90 azalt
Layer caching stratejisi ile build süresini optimize et — bağımlılıkları önce, kodu sonra kopyala
Spring Boot projeleri için layered JAR kullan — sadece değişen katmanlar yeniden build edilir
Python projeleri için virtual environment'ı builder'dan kopyala — build araçları runtime'a geçmez
Güvenlik: non-root kullanıcı, minimal base image, secret yönetimi, düzenli tarama
.dockerignore ile build context'i temiz tut
Docker image boyutu sadece disk alanı meselesi değil — deployment hızını, güvenliği ve operasyonel maliyetleri doğrudan etkiler. 800MB'lık bir image'ı 150MB'a indirmek, CI/CD pipeline'ını hızlandırır, container başlatma süresini kısaltır ve saldırı yüzeyini daraltır. Multi-stage build bunu sağlamanın en kolay ve en etkili yolu.
Bu yazıyı beğendiniz mi?
Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.
İlgili Yazılar
Docker Compose: Çoklu Container Uygulamalarını Tek Komutla Yönetme Rehberi
Docker Compose ile birden fazla container'ı nasıl tanımlar, yapılandırır ve tek bir komutla ayağa kaldırırsınız? Servis...
Docker Compose Rehberi: Çoklu Container Yönetimi
Docker Compose nedir, nasıl kurulur, docker-compose.yml nasıl yazılır? Services, volumes, networks, environment variable...
Docker Nedir? Container Teknolojisi Rehberi
Docker nedir, container nedir? VM vs container farkları, Dockerfile, Docker Compose, güvenlik ve Kubernetes karşılaştırm...