УРОК 22 / 30 0%

Docker + Docker Compose

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 Docker — зачем
  • Одинаковая среда везде (dev, CI, staging, prod) — «у меня работает» больше не аргумент
  • Изоляция — приложение не зависит от того, что стоит на хосте
  • Версионированиеtaskflow:0.1.0, можно откатиться
  • CI/CD — собираем образ, пушим в registry, деплоим на любой хост
2 Слои Docker-образа

[ Base image ]              ← eclipse-temurin:21-jdk-alpine
[ Build tools ]            ← Gradle, исходники
[ Dependencies ]            ← ./gradlew dependencies (кешируется)
[ Source code ]            ← .java файлы
[ Build artifacts ]        ← JAR
[ Runtime ]                ← JRE (для запуска)
[ Final image ]            ← только JAR + JRE

Multi-stage build — сначала собираем с JDK, потом копируем JAR в JRE-образ. Финальный — ~250 MB вместо 800 MB.

3 Docker Compose

Compose — инструмент для запуска многосервисного приложения одной командой:

  • app (TaskFlow)
  • postgres
  • kafka
  • prometheus (опционально)

Всё через файл docker-compose.yml.

---

💻

Практика: улучшаем CRUD

  1. 1
    Dockerfile (multi-stage)
    
    # Stage 1: Build
    FROM eclipse-temurin:21-jdk-alpine AS builder
    WORKDIR /build
    
    # Кешируем зависимости отдельно
    COPY gradlew settings.gradle.kts build.gradle.kts ./
    COPY gradle ./gradle
    RUN chmod +x ./gradlew && ./gradlew dependencies --no-daemon
    
    # Копируем исходники и собираем
    COPY src ./src
    RUN ./gradlew bootJar --no-daemon
    
    # Stage 2: Runtime
    FROM eclipse-temurin:21-jre-alpine
    RUN addgroup -S taskflow && adduser -S taskflow -G taskflow
    
    WORKDIR /app
    COPY --from=builder /build/build/libs/*.jar app.jar
    
    USER taskflow
    
    # Healthcheck для Docker/K8s
    HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
      CMD wget -qO- http://localhost:8080/actuator/health/liveness || exit 1
    
    EXPOSE 8080
    ENTRYPOINT ["java", \
      "-XX:+UseContainerSupport", \
      "-XX:MaxRAMPercentage=75.0", \
      "-Djava.security.egd=file:/dev/./urandom", \
      "-jar", "app.jar"]
    
  2. 2
    .dockerignore
    
    .git
    .gitignore
    .idea
    *.iml
    .gradle
    build
    out
    *.log
    .DS_Store
    node_modules
    
  3. 3
    Сборка
    
    docker build -t taskflow:0.1.0 .
    docker images | grep taskflow
    # taskflow  0.1.0  245 MB
    
  4. 4
    docker-compose.yml
    
    version: '3.8'
    
    services:
      postgres:
        image: postgres:16-alpine
        container_name: taskflow-postgres
        environment:
          POSTGRES_DB: taskflow_dev
          POSTGRES_USER: taskflow
          POSTGRES_PASSWORD: dev_password
        ports: ["5432:5432"]
        volumes:
          - pgdata:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U taskflow"]
          interval: 10s
          timeout: 5s
          retries: 5
    
      zookeeper:
        image: confluentinc/cp-zookeeper:7.6.0
        environment:
          ZOOKEEPER_CLIENT_PORT: 2181
    
      kafka:
        image: confluentinc/cp-kafka:7.6.0
        depends_on: [zookeeper]
        ports: ["9092:9092"]
        environment:
          KAFKA_BROKER_ID: 1
          KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
          KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
          KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    
      app:
        build: .
        image: taskflow:0.1.0
        container_name: taskflow-app
        depends_on:
          postgres: { condition: service_healthy }
          kafka:    { condition: service_started }
        environment:
          SPRING_PROFILES_ACTIVE: dev
          SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/taskflow_dev
          SPRING_DATASOURCE_USERNAME: taskflow
          SPRING_DATASOURCE_PASSWORD: dev_password
          SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
        ports: ["8080:8080"]
        healthcheck:
          test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health/liveness"]
          interval: 30s
          timeout: 5s
          retries: 3
          start_period: 30s
    
    volumes:
      pgdata: {}
    
  5. 5
    Запуск
    
    # Поднять всё
    docker compose up -d
    
    # Логи
    docker compose logs -f app
    docker compose logs -f kafka
    
    # Статус
    docker compose ps
    
    # Остановить
    docker compose down
    
    # Остановить + удалить volumes
    docker compose down -v
    
  6. 6
    Тест
    
    # Health check
    $ curl http://localhost:8080/actuator/health
    {"status":"UP"}
    
    # Регистрация
    $ curl -X POST http://localhost:8080/api/auth/register \
      -H "Content-Type: application/json" \
      -d '{"username":"alice","email":"a@e.com","password":"password123"}'
    
    # Логин
    $ curl -X POST http://localhost:8080/api/auth/login \
      -H "Content-Type: application/json" \
      -d '{"username":"alice","password":"password123"}'
    
  7. 7
    Логи
    
    # Следить за app
    docker compose logs -f app | grep -E "Started|ERROR"
    
    # Внутри контейнера
    docker compose exec app sh
    $ ls /app
    $ java -version
    
  8. 8
    -XX:MaxRAMPercentage=75.0

    Берёт 75% доступной RAM контейнера как max heap. Docker ограничивает RAM через --memory=512m — JVM сама подстроится. Без этого — JVM возьмёт всю RAM хоста, что может убить контейнер.

  9. 9
    Multi-stage — почему важен размер
    • eclipse-temurin:21-jdk — 700 MB
    • eclipse-temurin:21-jre — 200 MB
    • Финальный образ с JRE + JAR = 250 MB
    • В K8s это меньше времени на pull при каждом поде

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

«У меня работает» → у всех работает — одинаковый образ от dev до prod.
Rollbackdocker run taskflow:0.0.9 если 0.1.0 сломан.
Resource limitsdeploy.resources.limits.memory=512Mi в K8s + MaxRAMPercentage = JVM не убьёт контейнер.
Healthcheck — Docker/K8s убьёт контейнер, если health check не проходит.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Docker container vs virtual machine — в чём разница?

Разбор внутри: image, container, JVM и зависимости приложения.

2
Что такое multi-stage build в Dockerfile?

Разбор внутри: builder stage, runtime stage и маленький production image.

3
Почему порядок COPY влияет на Docker cache?

Разбор внутри: слои, кеш Gradle и быстрая пересборка.

4
Зачем нужен docker-compose в dev-среде?

Разбор внутри: приложение, Postgres, Kafka и Adminer одной командой.

5
Что даёт HEALTHCHECK в Dockerfile?

Разбор внутри: проверка контейнера и связь с Actuator.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 23

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 21
🎉
Новый тир
Новый тир достигнут!