Docker ve container'lar
Container'ın ne olduğundan Spring Boot için production'a uygun bir Dockerfile'a, compose ile yerel ortamdan sinyal, bellek ve güvenlik tuzaklarına kadar Docker'ı mülakatta sistematik anlatmayı öğretir.
Konu özeti
Container nedir, VM'den farkı ne?
Container, kendi dosya sistemi, ağı ve süreç ağacı varmış gibi çalışan sıradan bir Linux sürecidir. Bu izolasyonu iki kernel özelliği sağlar:
- Namespace'ler sürecin ne gördüğünü sınırlar:
pid(kendi süreç ağacı, kendisi PID 1),net(kendi ağ arayüzü ve port'ları),mnt(kendi dosya sistemi görünümü),uts(hostname),ipc,user(container içindeki root'un host'ta başka bir kullanıcıya eşlenmesi). - cgroup'lar sürecin ne kadar kullandığını sınırlar: bellek, CPU, süreç sayısı (
pids), I/O.
VM ise hypervisor üzerinde kendi kernel'iyle açılan tam bir işletim sistemidir. Fark buradan çıkar: container'lar host kernel'ini paylaşır, bu yüzden saniyeden kısa sürede açılır, megabaytlarla ölçülür ve bir makinede yüzlercesi çalışır. Bedeli, izolasyonun daha zayıf olmasıdır: kernel açığı tüm container'ları etkiler ve Linux container'ı Linux kernel'i ister. Mac ve Windows'taki Docker Desktop bu yüzden arkada küçük bir Linux VM çalıştırır.
Image, container, layer
Image, salt okunur katmanlardan (layer) ve çalıştırma metadata'sından (ENTRYPOINT, ENV, kullanıcı, port) oluşan bir pakettir. Container, bir image'ın çalışan örneğidir: image'ın katmanları + üstte container'a özel yazılabilir bir katman + süreç. Aynı image'dan on container açılırsa alttaki katmanlar diskte bir kez durur (overlay dosya sistemi, copy-on-write). Container silinince yazılabilir katman da gider; kalıcı veri volume'a yazılır.
Dockerfile'daki her dosya sistemi değiştiren talimat (RUN, COPY, ADD) bir katman üretir. Build sırasında BuildKit her talimat için cache'e bakar: COPY için kopyalanan dosyaların içerik checksum'ı (mtime değil), RUN için sadece komut metni karşılaştırılır. Bir talimatın cache'i bozulursa ondan sonraki tüm talimatlar yeniden çalışır. Pratik kural: az değişen şeyi üste, sık değişeni alta koy. Java için bu, önce pom.xml ve bağımlılık indirme, sonra src kopyalama demektir.
Temel Dockerfile talimatları
| Talimat | Ne yapar |
|---|---|
FROM | Taban image; AS name ile bir build aşamasına isim verir |
WORKDIR | Sonraki talimatların çalışma dizini (yoksa oluşturur) |
COPY / ADD | Build context'ten dosya kopyalar; ADD ayrıca URL ve arşiv açma bilir, genelde COPY tercih edilir |
RUN | Build sırasında komut çalıştırır, sonucu katman olur |
ENV / ARG | Çalışma zamanı ortam değişkeni / sadece build zamanı değişkeni |
USER | Sonraki talimatların ve container sürecinin kullanıcısı |
EXPOSE | Belgeleme; port'u dışarı açmaz |
ENTRYPOINT / CMD | Container açılınca çalışan komut / varsayılan argümanlar |
HEALTHCHECK | Docker'ın container sağlığını yoklaması (Kubernetes bunu yok sayar, kendi probe'larını kullanır) |
ENTRYPOINT sabit çalıştırılabilir dosyayı, CMD ona verilen ve docker run image <args> ile ezilebilen varsayılan argümanları tanımlar. İkisi de exec form (JSON dizi) ile yazılmalıdır. Shell form (ENTRYPOINT java -jar app.jar) komutu /bin/sh -c altında çalıştırır; PID 1 shell olur, docker stop'un gönderdiği SIGTERM Java'ya ulaşmaz, Docker varsayılan 10 saniye bekleyip SIGKILL gönderir ve graceful shutdown hiç çalışmaz. Kubernetes'teki karşılığı ve preStop için bkz. Kitap Bölüm 15.
Spring Boot için multi-stage Dockerfile
Multi-stage build'de derleme bir aşamada (JDK, Maven, kaynak kod) yapılır, son image'a sadece çalışmak için gerekenler (JRE ve uygulama) kopyalanır. Derleme araçları ve kaynak kod son image'a girmez.
Spring Boot'un fat jar'ı katmanlı olarak açılabilir: bağımlılıklar, Spring Boot loader, snapshot bağımlılıklar ve uygulama kodu ayrı dizinlere çıkar, her biri ayrı bir Docker katmanı olur. Bir satır kod değiştiğinde sadece birkaç yüz KB'lık application katmanı yeniden üretilir ve registry'ye itilir; yüz MB'lık bağımlılık katmanı cache'ten gelir.
Sürüm farkına dikkat: Spring Boot 3.3'e kadar resmi yol java -Djarmode=layertools -jar app.jar extract idi ve image org.springframework.boot.loader.launch.JarLauncher (3.2 öncesinde org.springframework.boot.loader.JarLauncher) ile başlatılıyordu. 3.3 ile yeni tools jar mode'u geldi ve layertools deprecated oldu; güncel dokümanda sadece java -Djarmode=tools -jar app.jar extract --layers --destination extracted yolu anlatılıyor ve çıkan dizindeki ince jar java -jar ile çalıştırılıyor. 4.x serisinde layertools'un kaldırıldığı bildiriliyor; kullandığın sürümün dokümanına bakmadan komutu ezberden yazma.
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk AS build
WORKDIR /workspace
# Önce az değişen dosyalar: bağımlılık katmanı cache'te kalsın
COPY mvnw pom.xml ./
COPY .mvn/ .mvn/
RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -q dependency:go-offline
COPY src/ src/
# Testler CI'da image build'inden önce koşar
RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -q package -DskipTests \
&& cp target/*.jar application.jar \
&& java -Djarmode=tools -jar application.jar extract --layers --destination extracted
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN groupadd --system --gid 10001 app \
&& useradd --system --uid 10001 --gid app --no-create-home --shell /usr/sbin/nologin app
# Dosyalar root'a ait ve salt okunur kalır; uygulama kendi kodunu değiştiremez
COPY --from=build /workspace/extracted/dependencies/ ./
COPY --from=build /workspace/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/extracted/application/ ./
USER 10001:10001
EXPOSE 8080
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=70 -XX:+ExitOnOutOfMemoryError"
ENTRYPOINT ["java", "-jar", "application.jar"]
Notlar: USER'ın sayısal verilmesi Kubernetes'in runAsNonRoot kontrolünün kullanıcıyı doğrulayabilmesini sağlar. Gradle'da COPY satırları gradlew, settings.gradle(.kts), build.gradle(.kts) ve gradle/ olur, jar build/libs/ altına düşer; Spring Boot Gradle eklentisi yanına bir de -plain.jar ürettiği için build/libs/*.jar glob'u iki dosyaya eşleşebilir, dosya adını açık ver ya da jar görevini kapat. MaxRAMPercentage değerinin neden 70 civarı olduğu ve heap dışı bellek bütçesi için bkz. Kitap Bölüm 3.
Alternatif: Dockerfile hiç yazmamak. ./mvnw spring-boot:build-image ya da ./gradlew bootBuildImage, Cloud Native Buildpacks (Paketo) ile katmanlı, root olmayan kullanıcıyla çalışan ve JVM bellek ayarını container limitine göre hesaplayan bir image üretir. Bedeli, image içeriği üzerinde daha az kontrol ve build makinesinde bir Docker daemon ihtiyacıdır. Jib de daemon gerektirmeden image üretir.
Build context ve .dockerignore
docker build . komutundaki . build context'tir: COPY'nin görebildiği her şey buradan gelir. .dockerignore olmadan .git, target/, IDE dosyaları ve yanlışlıkla .env dosyası da builder'a gönderilir; build yavaşlar, COPY . . kullanılıyorsa cache gereksiz bozulur ve secret image'a girebilir.
.git
.idea
*.iml
target/
build/
.gradle/
.env
*.log
Volume, bind mount, network
- Named volume (
-v pgdata:/var/lib/postgresql/data): Docker'ın yönettiği alan; container silinse de kalır. Veritabanı verisi için doğru seçim. - Bind mount (
-v ./config:/app/config): host'taki bir dizini olduğu gibi bağlar. Geliştirmede kod ya da konfigürasyon için kullanışlı, ama host'un dizin yapısına ve dosya izinlerine bağımlıdır.
Ağda varsayılan sürücü bridge'dir. -p 8080:8080 host'un 8080 port'unu container'ın 8080 port'una yönlendirir (sol taraf host, sağ taraf container). Kullanıcı tanımlı bir bridge ağında (compose her proje için otomatik bir tane kurar) container'lar birbirini servis adıyla bulur; Docker'ın gömülü DNS'i postgres adını o container'ın IP'sine çözer. Container içinden localhost, container'ın kendisidir; host ya da başka bir container değildir. Container'lar arası trafik container port'una gider; host port'u sadece dışarıdan erişim içindir.
Compose ile yerel ortam
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/orders
SPRING_DATASOURCE_USERNAME: orders
SPRING_DATASOURCE_PASSWORD: orders
SPRING_DATA_REDIS_HOST: redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
stop_grace_period: 40s
postgres:
image: postgres:17
environment:
POSTGRES_DB: orders
POSTGRES_USER: orders
POSTGRES_PASSWORD: orders
ports:
- "127.0.0.1:5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
pgdata:
depends_on tek başına sadece başlatma sırasını belirler; Postgres container'ı başladı diye bağlantı kabul ediyor değildir. condition: service_healthy, bağımlılığın healthcheck'i geçmesini bekler (diğer değerler service_started ve service_completed_successfully). $$, compose'un değişkeni kendisi genişletmemesi ve container içindeki shell'e bırakması içindir. Dosyanın başına version: yazmak artık gereksizdir. Postgres 18 ve sonrası image'larında veri dizini düzeni değişti; büyük sürüm atlarken mount yolunu image dokümanından kontrol et. Spring Boot 3.1'den beri spring-boot-docker-compose modülü, uygulama açılırken compose.yaml'ı kendisi başlatıp bağlantı bilgilerini otomatik verebilir.
Image boyutu, güvenlik, registry
- Taban image: JDK yerine JRE;
-alpinevaryantları küçüktür ama glibc yerine musl kullanır (native kütüphanelerde ve DNS davranışında sürpriz olabilir). Distroless image'larda shell ve paket yöneticisi yoktur: saldırı yüzeyi ve CVE sayısı düşer, amadocker exec ... shile içeri girip bakamazsın.jlinkile sadece gereken modülleri içeren bir runtime da üretilebilir. - Root olmayan kullanıcı: Container içindeki root, user namespace kullanılmıyorsa host'taki root ile aynı UID'dir; bir kaçış açığında etkisi büyür.
- Secret'lar image'a girmez.
ARGveENVdeğerleri image metadata'sında ve geçmişinde kalır; sonraki bir katmanda silinen dosya önceki katmanda durur. Build sırasında gereken token için BuildKit secret mount (RUN --mount=type=secret,...), çalışma zamanı secret'ları için ortamın secret mekanizması (Kubernetes Secret, Vault) kullanılır. - Tarama: Trivy, Grype ya da Docker Scout ile CI'da image taranır; taban image düzenli güncellenmezse dün temiz olan image bugün kritik CVE içerir.
- Tag'leme:
latestsadece bir etikettir, "en yeni" garantisi vermez ve yerinden oynar. Production'da değişmez tag'ler (1.4.3,1.4.3-a1b2c3dgibi git SHA'lı) ya da doğrudan digest (image@sha256:...) kullanılır. Staging'de test edilen digest'in aynısı production'a gider (build once, deploy many; bkz. Kitap Bölüm 15).
Sorular (23)
Image ile container arasındaki fark nedir?
Dockerfile nedir, temel talimatları nelerdir?
docker run -p 8080:8080 ne yapar? EXPOSE ile farkı nedir?
Volume ile bind mount arasındaki fark nedir?
Docker Compose ne işe yarar?
Compose'da uygulama container'ı veritabanına neden localhost ile bağlanamaz?
latest tag'ini kullanmanın sakıncası nedir?
Container'ın log'ları nereye yazılmalı, nasıl okunur?
Container başlar başlamaz neden kapanıyor?
Layer cache nasıl çalışır? Dockerfile'da talimat sırası neden önemli?
Multi-stage build nedir? Spring Boot için neden kullanılır?
ENTRYPOINT ile CMD arasındaki fark nedir?
Spring Boot uygulaması için iyi bir Dockerfile nasıl yazarsın?
Build context nedir? .dockerignore neden gerekir?
Container neden root kullanıcıyla çalışmamalı?
Uygulama veritabanından önce açılıp bağlantı hatasıyla çöküyor. depends_on ve healthcheck ile nasıl çözersin?
Image boyutunu nasıl küçültürsün? Alpine ve distroless arasında nasıl seçersin?
docker stop sonrası uygulama 10 saniye bekleyip ölüyor, graceful shutdown çalışmıyor. Neden?
JVM container'da bellek limitini nasıl görür? Container neden OOMKilled oluyor?
Build sırasında özel bir Maven deposuna token gerekiyor. Secret'ı image'a sızdırmadan nasıl verirsin?
Kubernetes artık Docker kullanmıyor deniyor. Docker ile build ettiğin image Kubernetes'te nasıl çalışıyor?
CI'da image güvenliğini nasıl sağlarsın? Tarama, tag'leme ve tekrarlanabilir build
Çoktan seçmeli (15)
Container ile sanal makine (VM) arasındaki temel fark hangisidir?
Tek doğru cevap var.
Çalışan bir container içinde /app/config.yml dosyasını değiştirip container'ı silersen ne olur?
Tek doğru cevap var.
docker run -p 9090:8080 order-service komutu için hangisi doğrudur?
Tek doğru cevap var.
Dockerfile'daki EXPOSE 8080 talimatı ne yapar?
Tek doğru cevap var.
Compose dosyasında app ve postgres adlı iki servis var; postgres ports: ["15432:5432"] ile publish edilmiş. app container'ı veritabanına hangi URL ile bağlanmalı?
Tek doğru cevap var.
Container içindeki bir Spring Boot uygulaması log'larını nereye yazmalıdır?
Tek doğru cevap var.
docker ps -a çıktısında container'ın Exited (137) ile durduğunu görüyorsun. 137 neyi gösterir?
Tek doğru cevap var.
Bir Maven projesi için hangi Dockerfile sırası, sadece Java kodu değiştiğinde bağımlılıkların yeniden indirilmesini önler?
Tek doğru cevap var.
Dockerfile'da ENTRYPOINT ["java", "-jar", "app.jar"] ve CMD ["--spring.profiles.active=prod"] var. docker run image --server.port=9090 çalıştırılınca hangi komut çalışır?
Tek doğru cevap var.
Compose'da depends_on: [postgres] (kısa yazım) neyi garanti eder?
Tek doğru cevap var.
Multi-stage bir Dockerfile'da son aşama FROM eclipse-temurin:21-jre ile başlıyor ve build aşamasından sadece katmanlara ayrılmış jar içeriği kopyalanıyor. Son image'da ne bulunmaz?
Tek doğru cevap var.
ENTRYPOINT java -jar /app/app.jar (shell form) kullanılan bir container'da server.shutdown=graceful açık. docker stop sonrası 10 saniye beklenip container 137 ile kapanıyor. En olası neden nedir?
Tek doğru cevap var.
1 GiB bellek limiti olan bir container'da JVM'e -Xmx1g verilmiş. Yük altında en olası sonuç nedir?
Tek doğru cevap var.
Aşağıdakilerden hangileri bir build secret'ını image'a ya da image geçmişine sızdırabilir? (Birden fazla doğru var.)
Birden fazla doğru olabilir.
Kubernetes 1.24 ile dockershim kaldırıldı. Docker ile build edilip registry'ye push edilmiş bir Spring Boot image'ı için hangisi doğrudur?
Tek doğru cevap var.
Kaynaklar
- Spring Boot: Dockerfiles:
jarmode=toolsileextract --layers, resmi multi-stage örnek, CDS ve AOT cache varyantları. - Spring Boot 3.2: Container Images (layertools): eski
jarmode=layertoolsyolu veJarLauncherile başlatma. - Spring Boot Maven Plugin 3.3: Deprecated List:
includeLayerToolsayarının 3.3'teincludeToolslehine deprecated edilmesi. - Spring Boot: Cloud Native Buildpacks: Dockerfile'sız image üretimi ve katman desteği.
- Docker: Build cache invalidation:
COPYchecksum'ı,RUNcache'i ve zincirleme bozulma kuralları. - Docker: Build secrets:
RUN --mount=type=secretve build arg/env'in secret için neden uygun olmadığı. - Docker: JSONArgsRecommended build check: shell form'un sinyal iletmemesi ve exec form önerisi.
- Docker Compose: Control startup order:
depends_onkoşulları (service_started,service_healthy,service_completed_successfully) verestartseçeneği. - Docker Compose: Networking: servis adıyla DNS çözümleme ve host port ile container port ayrımı.