Problem: "mənim maşınımda işləyir" və 700 MB-lıq image
Developer jar faylını lokal qurur, tam JDK əsaslı image-ə köçürür və root kimi işlədir. Image böyükdür, koddakı hər dəyişiklik asılılıq layer-ini ləğv edir, platforma container-i aydın mesaj olmadan öldürür və production-da hansı Java versiyasının işlədiyini heç kim dəqiq bilmir. Docker mühitləri təkrarlana bilən etmək üçündür, amma diqqətsiz yazılmış Dockerfile qarışıqlığı sadəcə qutunun içinə köçürür.
Bu məqalədə Spring Boot 3 servisi üçün təkrar istifadə edə biləcəyiniz Dockerfile və praktikada əhəmiyyət daşıyan ətraf ayarlar izah olunur.
Əsas fikir
Image yalnız oxunan layer-lərin yığınıdır. Docker hər layer-i keşləyir və instruksiya ilə girişlər dəyişməyibsə, təkrar istifadə edir. Yaxşı Dockerfile nadir dəyişənləri (base image, asılılıqlar) tez-tez dəyişənlərdən (sizin sinifləriniz) əvvələ qoyur. İkinci fikir: göndərdiyiniz image yalnız tətbiqi işə salmaq üçün lazım olanı saxlamalıdır, onu qurmaq üçün lazım olanı yox. Multi-stage build hər ikisini verir.
Layered jar ilə multi-stage Dockerfile
Spring Boot icra olunan jar-ı layer-lərə bölə bilir: asılılıqlar, Spring Boot loader, snapshot asılılıqlar və tətbiq sinifləri. Adi commit-də yalnız sonuncu layer dəyişir, buna görə yenidən build və image push kiçik qalır. Extract əmri Spring Boot versiyasından asılıdır: Boot 3.3-dən başlayaraq java -Djarmode=tools -jar app.jar extract --layers istifadə edin (aşağıdakı kimi); daha əvvəlki versiyalarda java -Djarmode=layertools -jar app.jar extract. Layer qovluqlarının adları eynidir, amma Boot versiyanızın rəsmi sənədini yoxlayın.
# build stage
FROM eclipse-temurin:21-jdk AS build
WORKDIR /workspace
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN ./mvnw -q dependency:go-offline
COPY src src
RUN ./mvnw -q package -DskipTests
RUN java -Djarmode=tools -jar target/app.jar extract --layers --destination extracted
# runtime stage
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 10001 app
WORKDIR /app
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 app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Burada bir neçə seçim şüurludur:
pom.xmlkopyalanıb və asılılıqlarsrc-dən əvvəl yüklənir, buna görə mənbə kodundakı dəyişiklik asılılıqları yenidən yükləmir.- Runtime mərhələsi JRE image-dən istifadə edir. Kompilyator yoxdur, alətlər azdır, deməli həm kiçikdir, həm də daha az səthi açıq qoyur.
- Proses non-root istifadəçi ilə işləyir. Tətbiq ələ keçirilsə, hücumçu container içində root kimi başlamır.
- Testlər image build-ində işlədilmir. Onları image qurulmazdan əvvəl CI-da işlədin ki, iki dəfə ödəməyəsiniz.
Jar-ı açma əmrinin dəqiq formasını Spring Boot versiyanızın sənədində yoxlayın, çünki layer alətinin interfeysi buraxılışlar arasında dəyişib.
target/, .git/, IDE qovluqları və lokal .env fayllarını xaric edən .dockerignore faylı əlavə edin. Onsuz build context böyüyür və sirlər layer-lərə düşə bilər.
Container yaddaşı və JVM
Container limiti JVM heap ölçüsü ilə eyni şey deyil. Container-ə 1 GiB verib JVM-in heap-ı bunun böyük hissəsi qədər götürməsinə icazə versəniz, heap, metaspace, thread stack-ləri və native yaddaş limiti keçə bilər və kernel prosesi öldürür. Bu çox vaxt Java stack trace olmadan 137 exit kodu ilə yenidən başlayan container kimi görünür.
Müasir JVM-lər container limitini oxuyur. Heap-ı sabit -Xmx əvəzinə, ona nisbətən faiz bayrağı ilə idarə edin:
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=70"Heap-dən kənar yaddaş üçün yer saxlayın. Düzgün faiz thread sayından, direct buffer-lərdən və kitabxanalardan asılıdır, ona görə rəqəmi köçürməyin, öz servisiniz üçün ölçün. Heap-dən kənarda nələrin olduğunu JVM yaddaşı və garbage collection məqaləsində görə bilərsiniz.
Health check-lər
Orchestrator container-in canlı olub-olmadığını və trafik qəbul etməyə hazır olub-olmadığını bilməlidir. Spring Boot Actuator bunu liveness və readiness probe-ları ilə təqdim edir.
management: endpoint: health: probes: enabled: true endpoints: web: exposure: include: healthserver: shutdown: gracefulLiveness-i "ilişmişəmsə məni yenidən başlat", readiness-i "mənə hələ trafik göndərmə" mənasında istifadə edin. Yavaş xarici asılılığı liveness-ə qatmayın: baza əlçatmazdırsa, bütün instance-ları yenidən başlatmaq kömək etmir, bərpanı yalnız çətinləşdirir. Graceful shutdown platforma SIGTERM göndərəndə işləməkdə olan sorğuların bitməsinə imkan verir.
Lokal inkişaf üçün docker compose
Compose tətbiqi və asılılıqlarını bir faylda təsvir edir, belə ki, yeni developer PostgreSQL-i əllə quraşdırmaq əvəzinə bir əmr işlədir.
services: db: image: postgres:16-alpine environment: POSTGRES_DB: app POSTGRES_USER: app POSTGRES_PASSWORD: local-only ports: ["5432:5432"] healthcheck: test: ["CMD-SHELL", "pg_isready -U app"] interval: retries: 10 app: build: . depends_on: db: condition: service_healthy environment: SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/app SPRING_DATASOURCE_USERNAME: app SPRING_DATASOURCE_PASSWORD: local-only ports: ["8080:8080"]Health şərti olan depends_on-a diqqət edin. Sadə depends_on yalnız container-in başlamasını gözləyir, PostgreSQL-in bağlantı qəbul etməsini yox. Yuxarıdakı parol yalnız birdəfəlik lokal baza üçündür; commit olunan compose faylına heç vaxt real kredensiallar yazmayın.
Konfiqurasiya, siqnallar və log-lar
Container-lər dəyişdirilə bilən olduğu üçün konfiqurasiya kənardan gəlməlidir. Spring Boot mühit dəyişənlərini property-lərə çevirir (SPRING_DATASOURCE_URL dəyişəni spring.datasource.url olur), buna görə eyni image noutbukda, staging-də və production-da işləyə bilir. Log-ları standart çıxışa yazın və toplamağı platformaya həvalə edin; container daxilindəki log faylı onunla birlikdə itir.
Prosesin necə başlayıb dayandığına da diqqət edin. Nümunədəki kimi ENTRYPOINT-in exec formasından istifadə edin ki, JVM PID 1 olsun və SIGTERM-i birbaşa alsın. Shell formasında aralıq shell siqnalı uda bilər və platforma timeout-dan sonra container-i öldürür, işləməkdə olan sorğular da itir.
Tez-tez edilən səhvlər
- Base image üçün
latesttag-ından istifadə etmək; build-lər səssizcə dəyişir. - Asılılıqları həll etməzdən əvvəl bütün layihəni kopyalamaq; layer keşi pozulur.
- Lazım olmadığı halda root və yazıla bilən fayl sistemi ilə işləmək.
- Mühitə xas konfiqurasiyanı və ya sirləri image-in içinə yazmaq. Eyni image hər mühitdə işləməlidir; konfiqurasiya mühit dəyişənlərindən və ya mount olunmuş fayllardan gəlir.
-Xmx-i container limitindən böyük qoymaq.- Bazanın "qalxmış" olmasını "hazır" olması ilə eyniləşdirmək.
- Vəziyyəti container fayl sistemində saxlamaq. Container dəyişdiriləndə itir.
Güzəştlər
Distroless və ya çox kiçik base image-lər hücum səthini azaldır, amma shell olmadığı üçün debug çətinləşir. GraalVM ilə qurulan native image-lər tez qalxır və az yaddaş işlədir, amma build vaxtı uzundur və reflection-a çox bağlı kitabxanalar əlavə konfiqurasiya tələb edir. Əksər komandalar üçün layered jar ilə JRE image məntiqli başlanğıcdır, ölçülmüş səbəbiniz olanda daha ixtisaslaşmış variantlara keçə bilərsiniz.
Yoxlama siyahısı
- JRE runtime image və bərkidilmiş tag-larla multi-stage build.
- Asılılıq layer-i tətbiq siniflərindən ayrı keşlənir.
- Non-root istifadəçi və
.dockerignorefaylı. - Heap container limitinin faizi kimi təyin olunub.
- Liveness və readiness probe-ları, graceful shutdown.
- Tətbiqi bazası ilə lokal işə salan compose faylı.
Növbəti addım
- Testlərdə real bazanı Testcontainers ilə işlədin.
- Image-i çatdırılma axınına Java servisləri üçün CI/CD məqaləsində qoşun.
- Konteynerlərin yerini görmək üçün junior Java developer roadmap səhifəsinə baxın.