Когда вы впервые сталкиваетесь с задачей деплоя, процесс может показаться сложным и пугающим. Докер-образы, безопасность, container registry, а тем более Kubernetes — для новичка это настоящая головная боль. Именно поэтому наши партнеры из Amplicode решили написать статью, которая поможет вам максимально просто и быстро задеплоить ваше первое Spring-приложение в облако.
Если вам удобнее смотреть, а не читать — вы можете ознакомиться с этим материалом в видео-формате.
❯ Описание приложения
Наше приложение — это стандартный пример Spring Petclinic, используемый для демонстрации возможностей Spring Boot. Помимо самого приложения нам также понадобятся два внешних сервиса: база данных PostgreSQL и брокер сообщений Kafka. Конечно, мы могли бы воспользоваться готовыми облачными сервисами, получив простоту настройки и быстрый старт. Однако сегодня мы пойдем другим путем: развернем все сервисы самостоятельно через Docker Compose.

❯ Контейнеризация Spring Boot приложения
Первый шаг — это создание файла docker-compose.yml:

В этот файл мы добавляем наше Spring Boot приложение, указываем параметры запуска и экспоузим необходимые порты. Современные IDE, а также плагины для них позволяют это сделать довольно просто. Воспользуемся действиями от Amplicode:

Сгенерированные файлы
services:
spring-petclinic:
image: org.springframework.samples.spring-petclinic:latest
build:
context: ../..
dockerfile: existing-spring-boot-app-modification/spring-petclinic-main/Dockerfile
args:
DOCKER_BUILDKIT: 1
restart: "no"
ports:
- "8080:8080"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
labels:
amplicode.image: springboot
FROM gradle:9.0.0 AS builder
WORKDIR /application
COPY . .
RUN --mount=type=cache,target=/root/.gradle gradle clean :org.springframework.samples.spring-petclinic:build -x test
FROM bellsoft/liberica-openjre-alpine:23 AS layers
WORKDIR /application
COPY --from=builder /application/existing-spring-boot-app-modification/spring-petclinic-main/build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM bellsoft/liberica-openjre-alpine:23
VOLUME /tmp
RUN adduser -S spring-user
USER spring-user
WORKDIR /application
COPY --from=layers /application/dependencies/ ./
COPY --from=layers /application/spring-boot-loader/ ./
COPY --from=layers /application/snapshot-dependencies/ ./
COPY --from=layers /application/application/ ./
ENV JAVA_RESERVED_CODE_CACHE_SIZE="240M"
ENV JAVA_MAX_DIRECT_MEMORY_SIZE="10M"
ENV JAVA_MAX_METASPACE_SIZE="179M"
ENV JAVA_XSS="1M"
ENV JAVA_XMX="345M"
ENV JAVA_ERROR_FILE_OPTS="-XX:ErrorFile=/tmp/java_error.log"
ENV JAVA_HEAP_DUMP_OPTS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp"
ENV JAVA_ON_OUT_OF_MEMORY_OPTS="-XX:+CrashOnOutOfMemoryError"
ENV JAVA_NATIVE_MEMORY_TRACKING_OPTS="-XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics"
ENV JAVA_GC_LOG_OPTS="-Xlog:gc*,safepoint:/tmp/gc.log::filecount=10,filesize=100M"
ENV JAVA_FLIGHT_RECORDING_OPTS="-XX:StartFlightRecording=disk=true,dumponexit=true,filename=/tmp/,maxsize=10g,maxage=24h"
ENV JAVA_JMX_REMOTE_OPTS="-Djava.rmi.server.hostname=127.0.0.1 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.port=5005 \
-Dcom.sun.management.jmxremote.rmi.port=5005"
ENTRYPOINT java \
-XX:ReservedCodeCacheSize=$JAVA_RESERVED_CODE_CACHE_SIZE \
-XX:MaxDirectMemorySize=$JAVA_MAX_DIRECT_MEMORY_SIZE \
-XX:MaxMetaspaceSize=$JAVA_MAX_METASPACE_SIZE \
-Xss$JAVA_XSS \
-Xmx$JAVA_XMX \
$JAVA_HEAP_DUMP_OPTS \
$JAVA_ON_OUT_OF_MEMORY_OPTS \
$JAVA_ERROR_FILE_OPTS \
$JAVA_NATIVE_MEMORY_TRACKING_OPTS \
$JAVA_GC_LOG_OPTS \
$JAVA_FLIGHT_RECORDING_OPTS \
$JAVA_JMX_REMOTE_OPTS \
org.springframework.boot.loader.JarLauncher
Обратите внимание, что в процессе создания сервиса для compose, мы сгенерировали и настроили Dockerfile согласно лучшим практикам написания подобных файлов для продакшена. Однако если у нас уже есть Dockerfile мы могли выбрать его:

Или даже уже собранный образ приложения:

Отмечу, что в процессе создания Dockerfile мы сразу добавляем настройки памяти JVM, чтобы приложение стабильно работало и не выходило за пределы доступной памяти контейнера.

Также заранее настраиваем инструменты для анализа и отладки: heap dump, GC-логи, Java Flight Recorder (JFR) и JMX. Эти инструменты помогут нам эффективно диагностировать проблемы в будущем (если вдруг когда они появятся).

Важный нюанс: по умолчанию я использую временные директории (например, /tmp), которые очищаются после перезапуска контейнера или виртуальной машины. Для продакшн-среды рекомендую подключить постоянный volume или сетевой диск.
❯ Создание PostgreSQL и Kafka сервисов для Docker Compose
Помимо приложения, в docker-compose.yml объявляем сервисы PostgreSQL и Kafka. В продакшн-среде не рекомендуется экспоузить порты этих сервисов напрямую наружу — достаточно, чтобы доступ к ним имело только ваше приложение.

Сгенерированные сервисы
services:
postgres:
image: postgres:17.5
restart: "no"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: spring-petclinic
healthcheck:
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
interval: 10s
timeout: 5s
start_period: 10s
retries: 5
kafka:
image: confluentinc/cp-kafka:8.0.0
restart: "no"
volumes:
- kafka_data:/var/lib/kafka/data
environment:
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_NODE_ID: 1
CLUSTER_ID: koa_uLJGTQe-qKkjpbagQg
KAFKA_PROCESS_ROLES: controller,broker
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://0.0.0.0:9092,CONTROLLER://kafka:9093
healthcheck:
test: kafka-topics --bootstrap-server localhost:9092 --list
interval: 10s
timeout: 5s
start_period: 30s
retries: 5
labels:
amplicode.image: confluent/kafka
volumes:
postgres_data:
kafka_data:
❯ Дополнительная конфигурация Spring Boot приложения
Чтобы наше приложение корректно работало в Docker-контейнере и могло гибко принимать параметры извне, необходимо внести небольшие изменения в настройки самого приложения. В частности, значения, такие как URL подключения к базе данных PostgreSQL, логин и пароль, а также адрес брокера Kafka, мы вынесем в переменные окружения. Это легко реализовать с помощью функции «Wrap properties into» от Amplicode, которая автоматически заменит статичные значения в файле свойств (application.properties) на переменные окружения.

Затем нам останется только передать эти переменные в Docker Compose сервис нашего приложения.

❯ Разворачиваем приложение в облаке
После того, как Dockerfile и docker-compose файл готовы, коммитим весь код в удаленный репозиторий. Например, в GitFlic. Затем переходим к облачной инфраструктуре. Я использую виртуальную машину в Timeweb Cloud, выбирая готовый образ с установленными Docker и Git, чтобы не тратить лишнее время на настройку окружения.

Все остальные настройки оставляю по умолчанию. После создания виртуальной машины подключаюсь к ней по SSH:

Остается только клонировать репозиторий и запустить приложение командой docker-compose up -d
:

Проверяем приложение на публичном ip.

Создадим несколько новых записей, и убедимся, что в логах виднеется корректная работа самого приложения и его взаимодействия со сторонними сервисами.

Всё работает великолепно!
❯ Итог
Конечно, такой подход далек от идеального продакшена, однако он отлично подходит для быстрого старта. С его помощью вы сможете на практике разобраться в основах деплоя и понять, как устроены докер-контейнеры и запуск приложений в облаке. В следующей статье мы рассмотрим более «взрослый» способ деплоя — с использованием Kubernetes.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩