← Kursa Dön
📄 Text · 30 min

Dil Bazlı Optimizasyon — Java, Node.js, Go, Python

Bir önceki derste multi-stage build'in temel mantığını öğrendik. Ama her programlama dilinin Docker image'ı optimize etmek için kendine özgü hileleri var. Node.js'in node_modules sorunu Java'dan çok farklı, Go'nun statik binary avantajı Python'da yok.

Farklı ülkelere kargo gönderiyorsun diyelim. Japonya'ya kompakt paketleme lazım. ABD'ye standart kutu yeter. Her ülkenin kuralı farklı — tek tip paketleme çalışmaz. Docker image'ları da öyle: her dilin Dockerfile'ı farklı optimize edilmeli.


Node.js Optimizasyonu

Node.js'in en büyük düşmanı: node_modules. Binlerce dosya, yüzlerce megabyte. Bu canavarı doğru yönetmek image boyutunu %70+ düşürür.

Production-Ready Node.js Dockerfile

# === Stage 1: Dependencies ===
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# === Stage 2: Build (TypeScript varsa) ===
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# === Stage 3: Production ===
FROM node:20-alpine AS production
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]

Üç ayrı stage kullandık ve her birinin amacı farklı. İlk stage sadece production dependencies yüklüyor — devDependencies yok. İkinci stage hem dev hem prod dependencies ile build yapıyor — TypeScript compile etmek için TypeScript compiler lazım. Üçüncü stage'de ise birinci stage'den production node_modules'ü, ikinci stage'den compile edilmiş kodu alıyoruz.

Node.js Spesifik İpuçları

`npm ci` vs `npm install`: Her zaman npm ci kullan. Bu komut package-lock.json'dan birebir yükler — deterministic ve daha hızlı. npm install ise lock dosyasını güncelleyebilir.

`--only=production`: devDependencies (TypeScript, test framework'ler, linter'lar) olmadan yükler. Boyut farkı dramatik — 400MB vs 120MB.

BuildKit cache mount: npm'in indirme cache'ini build'ler arası korur:

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

İlk build'de paketler indirilir ve cache'e yazılır. Sonraki build'lerde package.json değişse bile paketler cache'ten gelir — network indirmesi yok, çok daha hızlı.

Signal handling — tini: Node.js PID 1 olarak çalıştığında SIGTERM gibi sinyalleri doğru handle edemeyebilir. tini bu sorunu çözer:

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

Next.js standalone output: Next.js projelerinde next.config.js'e output: 'standalone' ekle. Bu, sadece gerekli dosyaları içeren minimal bir output üretir — tüm node_modules yerine sadece kullanılan modüller:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Boyut: ~100MB (tüm node_modules yerine sadece gerekli modüller).


Java Optimizasyonu

Java'nın iki büyük sorunu var. Birincisi JDK vs JRE — build araçları (JDK, Maven/Gradle) runtime'da gereksiz. İkincisi fat JAR — her şey tek dosyada ama layer cache'i çalışmaz.

JDK vs JRE Farkı

# Boyut karşılaştırması
eclipse-temurin:21          # JDK — 360MB (build için)
eclipse-temurin:21-jre      # JRE — 250MB (runtime için)
eclipse-temurin:21-jre-alpine  # JRE Alpine — 130MB 🏆

Production'da JDK'ya ihtiyacın yok — sadece JRE yeterli. Bu tek başına 200MB+ tasarruf.

JVM Memory Tuning

Container environment'ta JVM'in bellek ayarları kritik:

ENV JAVA_OPTS="-XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:InitialRAMPercentage=50.0 \
    -XX:+UseG1GC"
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

-XX:+UseContainerSupport JVM'e "sen container'dasın, container'ın bellek limitini oku" der. Yoksa JVM host'un tüm RAM'ini görüp aşırı bellek ayırabilir — OOM Kill!

Uygulamanın sadece belirli Java modüllerini kullanıyorsa, gereksiz modülleri çıkararak çok daha küçük bir JRE oluşturabilirsin:

FROM eclipse-temurin:21 AS jre-builder
RUN jlink \
    --add-modules java.base,java.logging,java.sql,java.naming \
    --strip-debug --no-man-pages --no-header-files \
    --compress=zip-6 --output /custom-jre

FROM alpine:3.19
COPY --from=jre-builder /custom-jre /opt/java
ENV PATH="/opt/java/bin:$PATH"
COPY --from=builder /app/target/app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]

Custom JRE: ~50MB (full JRE: ~200MB).

Maven/Gradle Cache

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

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

Go Optimizasyonu

Go, Docker optimizasyonunun şampiyonu. Statik binary oluşturur, hiçbir runtime bağımlılığı yok. scratch image ile 5-15MB mümkün.

Production-Ready Go Dockerfile

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s -X main.version=1.0.0" \
    -o /server ./cmd/server

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Go Spesifik İpuçları

`CGO_ENABLED=0`: C bağımlılığı olmayan statik binary oluşturur. scratch image'da çalışması için zorunlu.

`-ldflags="-w -s"`: -w DWARF debug bilgisini çıkarır, -s symbol table'ı çıkarır. Binary boyutunu ~%30 küçültür.

Build bilgisi gömme:

RUN go build -ldflags="-w -s \
    -X main.version=$(git describe --tags) \
    -X main.buildDate=$(date -u +%Y%m%dT%H%M%S)" \
    -o /server .

scratch vs distroless vs alpine:

ImageBoyutShellDebug
scratch0MBYokÇok zor
distroless/static2MBYokZor
alpine7MBVarKolay

Development'ta alpine, production'da scratch veya distroless kullan.

Go module cache:

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

İki cache mount: biri indirilen modüller için, biri compile cache için. Build süresini dramatik düşürür.


Python Optimizasyonu

Python'un zorlukları: C extension'lı paketler (numpy, psycopg2) build araçları ister, pip cache büyük olabilir, virtual environment karmaşası var.

Production-Ready Python Dockerfile

FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc libpq-dev && \
    rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.12-slim AS production
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"]

Python Spesifik İpuçları

`--no-cache-dir`: pip cache'ini image'da bırakma — gereksiz yer kaplar.

`PYTHONDONTWRITEBYTECODE=1`: .pyc dosyaları oluşturmaz — image boyutunu azaltır.

`PYTHONUNBUFFERED=1`: stdout/stderr buffer'lamaz — Docker loglarında çıktıyı anında görürsün.

Virtual environment yaklaşımı (alternatif):

FROM python:3.12-slim AS builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
CMD ["python", "app.py"]

Tüm venv dizinini kopyalamak, --user install'dan daha temiz ve taşınabilir.

Poetry kullanıyorsan:

FROM python:3.12-slim AS builder
RUN pip install poetry
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.in-project true && \
    poetry install --only main --no-interaction

pip cache mount:

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

Rust Optimizasyonu (Bonus)

Rust, Go gibi statik binary üretir ama compile süresi çok daha uzun. Cache stratejisi kritik:

FROM rust:1.77-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app

# Dependency cache trick — boş main ile sadece bağımlılıkları compile et
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release

# Gerçek kaynak kodu
RUN rm -rf src
COPY src ./src
RUN touch src/main.rs && cargo build --release

FROM scratch
COPY --from=builder /app/target/release/myapp /myapp
ENTRYPOINT ["/myapp"]

Boyut: 3-8MB. strip ile daha da küçültülebilir.


Boyut Karşılaştırma Tablosu

┌──────────────────────────────────────────────────────────────┐
│              Aynı Hello World API — Dil Karşılaştırması      │
├──────────────┬──────────────┬──────────────┬────────────────┤
│     Dil      │  Naive Build │  Optimized   │ Ultra-Optimized│
├──────────────┼──────────────┼──────────────┼────────────────┤
│  Node.js     │  1.1 GB      │  180 MB      │ 130 MB (distr) │
│  Java        │  700 MB      │  250 MB      │ 130 MB (jlink) │
│  Python      │  1.0 GB      │  200 MB      │ 150 MB (slim)  │
│  Go          │  850 MB      │  15 MB       │ 8 MB (scratch) │
│  Rust        │  1.5 GB      │  12 MB       │ 5 MB (scratch) │
└──────────────┴──────────────┴──────────────┴────────────────┘

Multi-Platform Build

Aynı Dockerfile'dan farklı CPU mimarileri (AMD64, ARM64) için build:

docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

Go'da platform-aware build:

FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /server .

Bu Derste Ne Öğrendik?

  • Node.js: npm ci --only=production + alpine + tini → 180MB.

  • Java: JRE-alpine + layered JAR + jlink custom JRE → 130-250MB.

  • Go: CGO_ENABLED=0 + ldflags="-w -s" + scratch → 8-15MB.

  • Python: --user install + slim + PYTHONDONTWRITEBYTECODE → 150-200MB.

  • BuildKit cache mount tüm dillerde build süresini dramatik düşürür.

  • Her dilin dependency cache pattern'ı farklı — go.mod, package.json, pom.xml, requirements.txt'i önce kopyala.

Sonraki derste layer caching stratejilerini derinlemesine inceleyeceğiz — build süresini 10 dakikadan 30 saniyeye düşürmenin yolları.