Dockerfile for Spring Boot
Giriş
Spring Boot uygulamalarını Docker ile paketlemek, modern deployment süreçlerinin temelidir. "Benim bilgisayarımda çalışıyor" problemini ortadan kaldırır — uygulama, bağımlılıkları ve çalışma ortamı birlikte paketlenir. Ancak basit bir Dockerfile yazmak kolaydır; production-ready bir Dockerfile yazmak ise dikkat gerektiren detaylarla doludur.
Bunu bir bavul hazırlamaya benzetebilirsiniz: tatil için bavul hazırlarken "birkaç kıyafet at, gitsin" diyebilirsiniz. Ama uzun bir iş seyahati için bavulunuzu düzenli, verimli ve güvenli hazırlarsınız — her şey doğru yerde, gereksiz eşya yok, kırılacak şeyler korunmuş. Production Docker image'ı da öyle olmalıdır.
Bu derste adım adım production-grade bir Dockerfile oluşturacağız: doğru base image seçimi, güvenlik yapılandırması, JVM bellek ayarları, image boyutu optimizasyonu ve health check'ler.
Base Image Seçimi
Spring Boot uygulamaları JVM üzerinde çalıştığından, uygun bir Java base image seçmek kritiktir. Yanlış seçim image boyutunu 3-4 kat artırabilir veya güvenlik açıkları oluşturabilir.
| Base Image | Boyut | Kullanım |
|---|---|---|
eclipse-temurin:21-jdk | ~340 MB | Build aşaması, geliştirme |
eclipse-temurin:21-jre | ~220 MB | Sadece çalıştırma |
eclipse-temurin:21-jre-alpine | ~120 MB | Production için ideal |
amazoncorretto:21-alpine | ~130 MB | AWS optimizasyonlu |
bellsoft/liberica-runtime-container:jre-21-musl | ~90 MB | En küçük boyut |
gcr.io/distroless/java21-debian12 | ~100 MB | Minimal saldırı yüzeyi |
JDK vs JRE
JDK (Java Development Kit):
├── javac (derleyici) ← Runtime'da gerekmez
├── javadoc ← Runtime'da gerekmez
├── jdb (debugger) ← Runtime'da gerekmez
├── jconsole ← Runtime'da gerekmez
└── JRE (Java Runtime) ← Uygulamayı çalıştırmak için yeterli
├── java (runtime)
├── Sınıf kütüphaneleri
└── JVMBest practice: Production'da JDK değil JRE kullanın. Uygulamanız derleme yapmaz, sadece çalışır.
Alpine vs Debian/Ubuntu
Alpine Linux (~5 MB):
✅ Çok küçük image boyutu
✅ Daha az güvenlik açığı yüzeyi
⚠️ musl libc kullanır (glibc değil)
⚠️ Bazı native kütüphanelerle (JNI) uyumluluk sorunu
Debian/Ubuntu (~80 MB):
✅ glibc — tüm kütüphanelerle tam uyumlu
✅ Daha fazla araç ve debug imkanı
❌ Daha büyük image boyutu
❌ Daha geniş saldırı yüzeyi💡 Tavsiye: Başlangıçta Alpine ile deneyin. Uyumluluk sorunu yaşarsanız Debian tabanlı image'a geçin. Çoğu Spring Boot uygulaması Alpine'da sorunsuz çalışır.
Basit Dockerfile — Başlangıç
# En temel Spring Boot Dockerfile'ı
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/myapp-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Bu çalışır ama production için yeterli değildir. Eksikler:
❌ Root kullanıcı ile çalışıyor (güvenlik riski)
❌ JVM bellek ayarı yok (container bellek limitini aşabilir)
❌ Sağlık kontrolü yok (orchestrator bilgilendirilemiyor)
❌
.dockerignoreyok (gereksiz dosyalar image'a giriyor)❌ Metadata yok (kim, ne zaman, hangi versiyon)
Production-Ready Dockerfile
# ============================================
# Production-Ready Spring Boot Dockerfile
# ============================================
FROM eclipse-temurin:21-jre-alpine
# ---- Metadata ----
LABEL maintainer="team@example.com"
LABEL org.opencontainers.image.title="order-service"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.description="Order management microservice"
# ---- Güvenlik: Non-root kullanıcı ----
RUN addgroup -S spring && adduser -S spring -G spring
# ---- Çalışma dizini ----
WORKDIR /app
# ---- Uygulama JAR'ını kopyala ----
COPY --chown=spring:spring target/myapp-0.0.1-SNAPSHOT.jar app.jar
# ---- Non-root kullanıcıya geç ----
USER spring:spring
# ---- Port belgele ----
EXPOSE 8080
# ---- Sağlık kontrolü ----
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
# ---- JVM bellek ayarları ile başlat ----
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-XX:+UseG1GC", \
"-XX:MaxGCPauseMillis=200", \
"-XX:+HeapDumpOnOutOfMemoryError", \
"-XX:HeapDumpPath=/tmp/heapdump.hprof", \
"-XX:+ExitOnOutOfMemoryError", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "app.jar"]Her Satırın Açıklaması
Non-Root User: Güvenlik
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:springContainer'lar varsayılan olarak root kullanıcısıyla çalışır. Bu ciddi bir güvenlik riskidir — container'dan kaçış (container escape) senaryosunda saldırgan host'ta root yetkisi kazanabilir.
# Alpine-based image'da
RUN addgroup -S spring && adduser -S spring -G spring
# Debian/Ubuntu-based image'da
RUN groupadd -r spring && useradd -r -g spring -s /sbin/nologin spring-S (Alpine) veya -r (Debian): Sistem kullanıcısı oluşturur (login shell yok, home dizini opsiyonel).
COPY --chown
COPY --chown=spring:spring target/*.jar app.jar--chown flag'i, dosyayı kopyalarken sahipliği değiştirir. Ayrı bir RUN chown komutuna gerek kalmaz — bu, bir Docker layer'ı tasarruf eder.
JVM Memory Flags
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-jar", "app.jar"]| Flag | Açıklama |
|---|---|
+UseContainerSupport | JVM'in container bellek limitlerini tanımasını sağlar (Java 10+ varsayılan açık) |
MaxRAMPercentage=75.0 | Heap için container belleğinin en fazla %75'ini kullan |
InitialRAMPercentage=50.0 | Başlangıçta %50 heap ayır |
Container Memory: 1 GB
├── Heap (MaxRAMPercentage=75%): 768 MB
└── Non-Heap: 256 MB
├── Metaspace: ~100 MB
├── Thread stacks: ~60 MB (30 thread × 2 MB)
├── Native memory: ~50 MB
└── OS + buffers: ~46 MB⚠️ Dikkat:
-Xmx/-Xmsyerine percentage-based ayarlar kullanmak, aynı image'ı farklı bellek limitleriyle çalıştırmanızı sağlar.docker run -m 512mveyadocker run -m 2g— JVM otomatik ayarlanır.
HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1| Parametre | Açıklama |
|---|---|
--interval=30s | Her 30 saniyede kontrol et |
--timeout=5s | 5 saniye içinde yanıt gelmezse başarısız |
--start-period=60s | İlk 60 saniye kontrolü atla (başlama süresi) |
--retries=3 | 3 ardışık başarısızlıkta "unhealthy" olarak işaretle |
Alpine image'da curl yoktur, wget kullanın. Veya curl yükleyin:
RUN apk add --no-cache curl
HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health || exit 1java.security.egd
-Djava.security.egd=file:/dev/./urandomJVM, güvenli rastgele sayı üretimi için /dev/random kullanır. Container ortamında entropi (rastgelelik kaynağı) düşük olabilir ve uygulama başlangıcı yavaşlayabilir. /dev/urandom daha hızlıdır ve çoğu kullanım için yeterince güvenlidir.
.dockerignore
.dockerignore dosyası, COPY ve ADD talimatlarının kopyalamaması gereken dosyaları belirler. Build context'ini küçülterek build süresini kısaltır:
# .dockerignore
.git
.gitignore
.idea
.vscode
*.iml
# Maven
.mvn/wrapper/maven-wrapper.jar
target/
!target/*.jar
# Gradle
.gradle
build/
!build/libs/*.jar
# Docker files
docker-compose*.yml
Dockerfile*
# Documentation
README.md
*.md
docs/
# Test
src/test/
# Environment
.env
.env.*
# OS files
.DS_Store
Thumbs.db💡 İpucu:
target/klasörünü hariç tutup!target/*.jarile sadece JAR dosyasını dahil etmek, gereksiz dosyaların image'a girmesini önler. Build context 500 MB → 50 MB düşebilir.
ENTRYPOINT vs CMD
# ENTRYPOINT — değiştirilemez ana komut
ENTRYPOINT ["java", "-jar", "app.jar"]
# docker run myapp → java -jar app.jar
# docker run myapp --server.port=9090 → java -jar app.jar --server.port=9090
# CMD — varsayılan, override edilebilir
CMD ["java", "-jar", "app.jar"]
# docker run myapp → java -jar app.jar
# docker run myapp sh → sh (tamamen override edilir)
# Birlikte kullanım — ENTRYPOINT sabit, CMD varsayılan argüman
ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["--spring.profiles.active=default"]
# docker run myapp → java -jar app.jar --spring.profiles.active=default
# docker run myapp --spring.profiles.active=prod → java -jar app.jar --spring.profiles.active=prodBest practice: Spring Boot uygulamalarında ENTRYPOINT kullanın. docker run myapp --server.port=9090 gibi argüman eklemeye izin verir.
Environment Variables ile Konfigürasyon
# Dockerfile'da varsayılan environment variable'lar
ENV SPRING_PROFILES_ACTIVE=default
ENV JAVA_OPTS=""
ENTRYPOINT ["sh", "-c", \
"java $JAVA_OPTS -jar app.jar"]# Çalıştırırken override
docker run -e SPRING_PROFILES_ACTIVE=prod \
-e JAVA_OPTS="-XX:MaxRAMPercentage=80.0" \
-e DATABASE_URL="jdbc:postgresql://db:5432/mydb" \
myapp:latest# docker-compose.yml ile
services:
app:
image: myapp:latest
environment:
SPRING_PROFILES_ACTIVE: prod
DATABASE_URL: jdbc:postgresql://db:5432/mydb
DB_PASSWORD: ${DB_PASSWORD} # .env dosyasındanBuild ve Çalıştırma
# 1. Uygulamayı derle
./mvnw clean package -DskipTests
# 2. Docker image oluştur
docker build -t myapp:1.0.0 .
# 3. Image boyutunu kontrol et
docker images myapp
# REPOSITORY TAG SIZE
# myapp 1.0.0 185MB
# 4. Çalıştır
docker run -d --name myapp \
-p 8080:8080 \
-m 512m \
-e SPRING_PROFILES_ACTIVE=dev \
myapp:1.0.0
# 5. Logları izle
docker logs -f myapp
# 6. Health check durumunu kontrol et
docker inspect --format='{{.State.Health.Status}}' myapp
# healthy
# 7. Container'a shell aç (debug)
docker exec -it myapp shDocker Image Güvenlik Taraması
Production'a göndermeden önce image'ınızı güvenlik açıkları için tarayın:
# Docker Scout ile tarama
docker scout cves myapp:1.0.0
# Trivy ile tarama (önerilen)
trivy image myapp:1.0.0
# Snyk ile tarama
snyk container test myapp:1.0.0# CI/CD'de otomatik tarama (GitHub Actions)
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'table'
exit-code: '1' # CVE bulunursa build fail
severity: 'CRITICAL,HIGH'Güvenlik İçin Best Practices
# 1. Belirli versiyon + SHA digest kullanın
FROM eclipse-temurin:21-jre-alpine@sha256:abc123def456
# 2. Gereksiz paketleri kurmayın
RUN apk add --no-cache curl # --no-cache: index cache silir
# 3. Distroless image kullanın (shell bile yok)
FROM gcr.io/distroless/java21-debian12
# Shell yok → saldırgan shell açamaz
# Paket yöneticisi yok → ek yazılım kurulamaz
# 4. Read-only filesystem
# docker run --read-only --tmpfs /tmp myapp:latestMulti-Architecture Build
Farklı CPU mimarileri (amd64, arm64) için image oluşturmak:
# Buildx ile multi-platform build
docker buildx create --name multiarch --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:1.0.0 \
--push .# Multi-arch uyumlu Dockerfile
FROM --platform=$TARGETPLATFORM eclipse-temurin:21-jre-alpine
# $TARGETPLATFORM otomatik olarak doğru mimariyi seçerBu, Apple Silicon (M1/M2/M3) Mac'lerde geliştirip x86 Linux sunuculara deploy eden ekipler için kritiktir.
Image Boyutu Karşılaştırması
Aynı Spring Boot uygulaması, farklı Dockerfile yaklaşımlarıyla:
Yaklaşım | Image Boyutu
------------------------------------|-------------
JDK + kaynak kodu (kötü) | ~800 MB
JDK-slim | ~400 MB
JRE (Debian) | ~280 MB
JRE-Alpine | ~180 MB
Multi-stage + JRE-Alpine | ~150 MB
Layered JAR + JRE-Alpine | ~150 MB (ama cache'leme daha iyi)
Distroless | ~160 MB
jlink custom JRE + Alpine | ~100 MBDocker Build Cache Optimizasyonu
Docker, her RUN, COPY, ADD talimatını bir layer olarak cache'ler. Bir layer değişirse ondan sonraki TÜM layer'lar yeniden oluşturulur. Bu nedenle sıralama önemlidir:
# ✅ DOĞRU sıralama — en az değişen → en çok değişen
FROM eclipse-temurin:21-jre-alpine
# 1. Sistem yapılandırması (neredeyse hiç değişmez)
RUN addgroup -S spring && adduser -S spring -G spring
WORKDIR /app
# 2. JVM ayarları (nadiren değişir)
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# 3. Uygulama JAR'ı (her build'de değişir)
COPY --chown=spring:spring target/*.jar app.jar
USER spring:spring
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]# ❌ YANLIŞ sıralama — JAR önce, user sonra
COPY target/*.jar app.jar # JAR her build'de değişir
RUN addgroup -S spring ... # Bu layer her seferinde yeniden oluşturulur!Gerçek Dünya Örneği: Tam Production Dockerfile
# ============================================
# Production Dockerfile — E-Ticaret Order Service
# ============================================
ARG JAVA_VERSION=21
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine
# Metadata
LABEL maintainer="platform-team@example.com"
LABEL org.opencontainers.image.source="https://github.com/myorg/order-service"
LABEL org.opencontainers.image.description="Order management microservice"
# Güvenlik: non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Gerekli araçlar (health check için)
RUN apk add --no-cache wget
# Timezone ayarı
RUN apk add --no-cache tzdata
ENV TZ=Europe/Istanbul
# Çalışma dizini
WORKDIR /app
# Uygulama
COPY --chown=appuser:appgroup target/*.jar app.jar
# Kullanıcı değiştir
USER appuser:appgroup
# Port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
# Başlatma komutu
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-XX:+UseG1GC", \
"-XX:MaxGCPauseMillis=200", \
"-XX:+HeapDumpOnOutOfMemoryError", \
"-XX:HeapDumpPath=/tmp/heapdump.hprof", \
"-XX:+ExitOnOutOfMemoryError", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "app.jar"]Yaygın Hatalar
1. Root Kullanıcı ile Çalıştırmak
# ❌ USER talimatı yok → root olarak çalışır
FROM eclipse-temurin:21-jre-alpine
COPY target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]2. JDK Kullanmak
# ❌ JDK — 340 MB, gereksiz araçlar
FROM eclipse-temurin:21-jdk
# ✅ JRE — 120 MB, sadece runtime
FROM eclipse-temurin:21-jre-alpine3. .dockerignore Eksikliği
# .dockerignore yoksa:
# - .git dizini image'a girer (100+ MB)
# - node_modules, target/ dizini image'a girer
# - Build context'i göndermek dakikalar sürer4. Latest Tag Kullanmak
# ❌ YANLIŞ — hangi versiyon olduğu belirsiz
FROM eclipse-temurin:latest
# ✅ DOĞRU — belirli versiyon
FROM eclipse-temurin:21-jre-alpine
# Veya SHA ile
FROM eclipse-temurin:21-jre-alpine@sha256:abc123...5. Gereksiz RUN Katmanları
# ❌ YANLIŞ — 3 katman
RUN apk update
RUN apk add curl
RUN rm -rf /var/cache/apk/*
# ✅ DOĞRU — 1 katman
RUN apk add --no-cache curl6. Shell Form ENTRYPOINT
# ❌ YANLIŞ — shell form (PID 1 sorunu)
ENTRYPOINT java -jar app.jar
# Bu aslında: /bin/sh -c "java -jar app.jar"
# JVM PID 1 değil, SIGTERM sinyalini alamaz → graceful shutdown çalışmaz
# ✅ DOĞRU — exec form
ENTRYPOINT ["java", "-jar", "app.jar"]
# JVM PID 1 olarak çalışır, SIGTERM alabilir7. Secrets'ı Image'a Gömmek
# ❌ YANLIŞ — image'da secret kalır
ENV DB_PASSWORD=supersecret123
COPY .env /app/.env
# ✅ DOĞRU — runtime'da environment variable olarak geçirin
# docker run -e DB_PASSWORD=supersecret123 myapp:latest
# veya Docker secrets / Kubernetes secrets kullanınDebugging ve Troubleshooting
# Container içindeki dosyaları listele
docker run --rm myapp:1.0.0 ls -la /app/
# Image layer'larını incele
docker history myapp:1.0.0
# Image manifest'ini görüntüle
docker inspect myapp:1.0.0
# Container'ın bellek kullanımını izle
docker stats myapp-container
# JVM heap dump al (debug)
docker exec myapp-container jcmd 1 GC.heap_dump /tmp/dump.hprof
docker cp myapp-container:/tmp/dump.hprof ./dump.hprofÖzet
Base image: Production'da JRE + Alpine tercih edin (
eclipse-temurin:21-jre-alpine)Non-root user:
adduser -S spring+USER spring:spring— güvenlik için zorunluJVM flags:
MaxRAMPercentage=75.0— container memory-aware ayarlarHEALTHCHECK: Actuator
/healthendpoint'i ile sağlık kontrolü.dockerignore: Build context'ini küçültün, gereksiz dosyaları hariç tutun
ENTRYPOINT:
CMDyerineENTRYPOINTkullanın — argüman ekleme esnekliğiVersiyonlama:
latesttag'i kullanmayın, belirli versiyon belirtin
AI Asistan
Sorularını yanıtlamaya hazır