Multi-Stage Build Temelleri — Neden, Nasıl?
Docker'la bir uygulama image'ı build ettiğinde, çoğu zaman sonuç gereksiz yere büyük olur. 50MB'lık bir Node.js uygulaması için 1.2GB'lık image? Bu mantıklı değil. İçinde TypeScript compiler, devDependencies, build cache, test framework'ler — hiçbirini runtime'da kullanmıyorsun ama hepsi image'da oturuyor.
Bir araba fabrikasını düşün. Üretim hattında kaynak makineleri, boyama kabinleri, test ekipmanları var. Ama müşteriye arabayı teslim ederken fabrikayı da vermiyorsun — sadece bitmiş arabayı. Docker'da da aynı mantık geçerli: build araçlarını final image'da bırakma. Multi-stage build ile "fabrika" aşamasında build et, "teslimat" aşamasında sadece çalışan uygulamayı al.
Problemi Görelim — Şişman Image
Önce problemi somutlaştıralım. Basit bir Node.js TypeScript projesi için tek stage Dockerfile yazalım:
# ❌ Tek stage — her şey image'da kalıyor
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]docker build -t myapp:fat .
docker images myapp:fatREPOSITORY TAG SIZE
myapp fat 1.2GB1.2GB! Bu image'ın içinde ne var?
Node.js runtime (150MB) — ✅ runtime'da gerekli
npm/yarn (50MB) — ❌ runtime'da gereksiz
node_modules devDependencies dahil (400MB) — ❌ çoğu gereksiz
TypeScript compiler (100MB) — ❌ compile bitti, artık gereksiz
Kaynak kodu .ts dosyaları (5MB) — ❌ compile edilmiş .js yeterli
Build cache, temp dosyaları — ❌ tamamen gereksiz
Yarısından fazlası gereksiz! Peki bunu nasıl çözeriz?
Multi-Stage Build — Çözüm
Multi-stage build'de birden fazla FROM talimatı kullanırsın. Her FROM yeni bir "stage" başlatır. Önceki stage'lerden sadece ihtiyacın olan dosyaları COPY --from ile alırsın. Sonuç: sadece son stage'deki katmanlar final image'a girer.
# ✅ Multi-stage — build ve runtime ayrı
# === Stage 1: Build ===
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# === Stage 2: Production ===
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]docker build -t myapp:slim .
docker images myapp:slimREPOSITORY TAG SIZE
myapp slim 180MB1.2GB → 180MB! %85 küçülme. Ve bu sadece iki basit değişiklikle oldu.
Nasıl Çalışıyor?
İlk stage'de (builder) TypeScript compile ediyoruz. Bu stage'de node:20 (1.1GB) base image kullanıyoruz çünkü build araçlarına ihtiyacımız var. Ama bu stage'in kendisi final image'a dahil edilmez.
İkinci stage'de (production) node:20-alpine (130MB) ile başlıyoruz — çok daha küçük. Sadece production dependencies yüklüyoruz ve builder stage'den compile edilmiş dist/ dizinini kopyalıyoruz.
Stage 1 (builder): Stage 2 (final):
┌─────────────────────┐ ┌─────────────────────┐
│ node:20 (1.1GB) │ │ node:20-alpine(130MB)│
│ + devDependencies │ │ + prod dependencies │
│ + TypeScript │ COPY dist │ + dist/ (compiled) │
│ + Source code │ ──────────→ │ │
│ + Build tools │ │ Toplam: ~180MB │
│ Toplam: ~1.2GB │ └─────────────────────┘
└─────────────────────┘
❌ Atılır ✅ Final imageBuilder stage build bitince "atılır" — host'ta cache olarak kalır ama final image'a girmez.
COPY --from — Stage'ler Arası Transfer
COPY --from komutunun birkaç kullanım şekli var:
# Named stage'den kopyala
COPY --from=builder /app/dist ./dist
# Harici bir image'dan kopyala (çok kullanışlı!)
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf
# Stage numarasından kopyala (0-indexed)
COPY --from=0 /app/output ./outputHarici image'dan kopyalama çok güçlü bir özellik. Mesela Nginx'in default config'ini alıp kendi image'ında kullanabilirsin.
Go ile Multi-Stage — Şampiyonlar Ligi
Go, multi-stage build'in en parlak örneğidir. Go statik binary oluşturur — hiçbir runtime bağımlılığı yok. Bu sayede scratch (tamamen boş) image ile kullanabilirsin:
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server ./cmd/server
# Stage 2: Scratch — tamamen boş image!
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]docker images myappREPOSITORY TAG SIZE
myapp go 12MB12MB! scratch image tamamen boş — içinde sadece Go binary ve SSL sertifikaları var. Shell yok, paket yöneticisi yok, hiçbir şey yok. Sadece uygulaman çalışıyor.
CGO_ENABLED=0 statik binary oluşturur (C kütüphanelerine bağımlılık yok). -ldflags="-w -s" debug bilgilerini çıkararak binary boyutunu ~%30 küçültür.
Java ile Multi-Stage — Spring Boot Layer Extraction
Java uygulamaları genelde büyük olur — JDK + Maven + dependencies. Multi-stage ile JDK'yı atıp sadece JRE kalır:
# Stage 1: Build
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
# Stage 2: Layer extraction (Spring Boot)
FROM eclipse-temurin:21-jre-alpine AS extractor
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# Stage 3: Production
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
WORKDIR /app
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Spring Boot'un layered jar özelliği çok akıllıca. Dependencies (nadiren değişir) ayrı katmanda, application kodu (sık değişir) ayrı katmanda. Sadece kodun değiştiğinde üstteki katmanlar cache'ten gelir — build çok daha hızlı.
Python ile Multi-Stage
Python'un zorluğu C extension'lı paketler — numpy, psycopg2 gibi paketler compile edilmek ister. Build araçlarını (gcc, headers) production'a taşımak istemezsin:
# Stage 1: Build
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Production
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
RUN useradd --create-home appuser
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY . .
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "-w", "4", "app:app"]Builder'da gcc ve libpq-dev (header dosyaları) var — compile için gerekli. Production'da sadece libpq5 (runtime library) var — gcc ve header'lar yok.
Named Stages ve Target Build
Stage'lere isim verince çok kullanışlı şeyler yapabilirsin:
FROM node:20 AS deps
# dependency stage
FROM node:20 AS builder
# build stage
FROM node:20 AS tester
COPY --from=builder /app/dist ./dist
RUN npm test
FROM node:20-alpine AS production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]# Sadece test stage'e kadar build et
docker build --target tester -t myapp:test .
# Production image
docker build --target production -t myapp:prod .CI/CD pipeline'ında: önce --target tester ile testleri çalıştır, geçerse --target production ile production image build et. Bu sayede test aşamasında production image oluşturmana gerek kalmaz — zaman kazanırsın.
Docker Compose'da:
services:
api:
build:
context: ./api
target: development # Dev stage'i build etBoyut Karşılaştırması
Aynı "Hello World" API uygulaması, farklı stratejilerle:
Strateji Node.js Go Java Python
─────────────────────────────────────────────────────────────────────
Tek stage, full image 1.1 GB 850 MB 700 MB 1.0 GB
Tek stage, alpine 350 MB 250 MB 350 MB 130 MB
Multi-stage, alpine 180 MB 15 MB 250 MB 200 MB
Multi-stage, distroless 150 MB 12 MB 220 MB 150 MB
Multi-stage, scratch — 8 MB — —Go ile scratch kullanarak 850MB → 8MB! %99 küçülme. Node.js ile multi-stage+alpine ile 1.1GB → 180MB — %84 küçülme.
Yaygın Hatalar
Stage ismi büyük/küçük harf duyarlı:
FROM node:20 AS Builder # Büyük B
COPY --from=builder ... # Küçük b → HATA!Alpine'da native modül sorunu: Alpine musl libc kullanır, bazı native modüller (bcrypt, sharp) sorun çıkarabilir. Çözüm: build stage'de full image, production'da alpine.
Dosya eksik kopyalama: Production stage'de dist/ kopyaladın ama node_modules unuttun → "module not found" hatası. COPY komutlarını dikkatlice kontrol et.
Bu Derste Ne Öğrendik?
Multi-stage build, build araçlarını final image'dan ayırarak boyutu drastik düşürür.
COPY --from=stage_nameile bir stage'den diğerine sadece gerekli dosyaları taşı.Go → scratch (8MB), Java → JRE-alpine (250MB), Node.js → alpine (180MB), Python → slim (200MB).
--targetile CI/CD'de farklı stage'leri build et — test ayrı, production ayrı.Her dil için multi-stage pattern farklı ama temel mantık aynı: build araçlarını production'a taşıma.
Sonraki derste dil bazlı optimizasyon detaylarına dalacağız — Node.js'te npm ci, Java'da jlink, Go'da ldflags, Python'da pip tricks.
AI Asistan
Sorularını yanıtlamaya hazır