middle
Как контейнеризировать и деплоить Java-приложение?
Контейнеризация Java-приложения строится на multi-stage Docker build с layered JAR, а деплой в production выполняется через Kubernetes с Helm/Kustomize.
Multi-stage Dockerfile
Dockerfile
# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts gradlew ./
COPY gradle/ gradle/
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
# Stage 2: Extract layers
FROM eclipse-temurin:21-jdk-alpine AS extractor
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
# Stage 3: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=extractor /app/extracted/dependencies/ ./
COPY --from=extractor /app/extracted/spring-boot-loader/ ./
COPY --from=extractor /app/extracted/snapshot-dependencies/ ./
COPY --from=extractor /app/extracted/application/ ./
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health/liveness || exit 1
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75.0", \
"org.springframework.boot.loader.launch.JarLauncher"]
Kubernetes Deployment
deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
template:
spec:
containers:
- name: order-service
image: ghcr.io/example/order-service:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 15
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 60
Graceful shutdown
Пример
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
server:
shutdown: graceful
Последовательность: SIGTERM -> preStop (sleep 10) -> readiness DOWN -> ожидание завершения запросов -> закрытие контекста.
GraalVM Native Image
| Критерий | JVM | Native Image |
|---|---|---|
| Старт | 2-10 секунд | Менее 100 мс |
| Память | Средняя-высокая | Низкая |
| Peak performance | Выше (JIT) | Ниже (AOT) |
| Подходит для | Long-running сервисы | Serverless, CLI, FaaS |
Частые ошибки
- Запуск контейнера от root — уязвимость
- Отсутствие resource limits: pod занимает весь node
- latest тег в production — неизвестно, какая версия запущена
- Отсутствие startup probe: liveness probe убивает pod, который ещё не стартовал
- MaxRAMPercentage=100% — не остаётся места для off-heap памяти, используйте 75%
На собеседовании: обязательные знания: multi-stage build (зачем три этапа), layered JAR (кэширование Docker-слоёв), non-root user, три типа probe (startup, liveness, readiness) и их назначение. Вопрос “Почему не latest?” — ответ: невозможно откатить и понять, что запущено. Используйте SHA или semver.