Gymterview
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.