1. «Работает у меня» — это ещё не свойство проекта
Фраза «у меня всё работает» звучит как хорошая новость только первые пару минут. Потом выясняется, что это новость об одной конкретной машине, а не о проекте. Репозиторий может быть один и тот же, но условия вокруг него разные: версия Java, системные переменные, локальные процессы на портах, доступность PostgreSQL, права на директории, рабочая папка процесса, привычки IDE. Из этих мелочей и собирается то самое знаменитое «ничего не работает».
Поэтому воспроизводимость — это не красивое слово, а вполне прикладное свойство проекта. Она означает, что запуск описан так, чтобы он не зависел от неочевидных договорённостей и случайных особенностей машины. README здесь помогает, но проблему целиком не решает. Он может объяснить, что вы хотели, но не может гарантировать, что среда действительно совпала с ожиданиями.
Если сказать ещё точнее, локальный запуск без воспроизводимости почти всегда оказывается смесью кода и удачи. Пока проект маленький и живёт у одного человека, это терпимо. Как только в картину добавляется второй разработчик, второй ноутбук или просто возвращение к проекту через месяц, цена этой удачи резко вырастает.
2. Сервис живёт в runtime-окружении
Это, пожалуй, главная мысль всей лекции. Сервис — это не только jar, не только классы и не только бизнес-логика. У сервиса есть условия жизни: порт, режим запуска, профиль, переменные окружения, файловые пути, внешние зависимости, минимальный health-сигнал. Если эти условия нигде не зафиксированы и не стабилизированы, сам сервис остаётся хрупким, даже если код написан аккуратно.
Вот почему один и тот же Boot-проект может быть «написан» и при этом «не готов к жизни». С точки зрения Java всё собрано. С точки зрения runtime — слишком много скрытых предпосылок. Где-то кто-то ожидает, что порт 8080 свободен. Где-то — что на машине уже поднята база. Где-то — что директория ./data/exports существует и доступна на запись. Где-то — что профиль приложения включён «сам собой».
И здесь как раз появляется тот самый момент, ради которого Docker вообще нужен. Проблема не в том, что вы пока не выучили нужную CLI-команду. Проблема в том, что ваш сервис ещё не оформлен как воспроизводимый runtime-объект. Docker приходит не на место Spring Boot, а на место хаотичной среды запуска.
3. Один сервис, разные режимы: standalone и postgres
На учебном сервисе это видно особенно хорошо. У нас не два разных приложения, а один и тот же Container-Ready Catalog Service, который может жить как минимум в двух режимах. В standalone режиме он стартует без внешней базы и хранит данные в памяти. Это удобно для быстрых проверок и ранних шагов курса. В postgres режиме у него уже появляется внешняя зависимость, и картина запуска становится другой.
На уровне конфигурации идея выглядит очень просто:
# application-standalone.yml
# Конфигурация для режима "без внешней БД": всё хранится в памяти процесса
app:
storage: in-memory # режим хранения (важно: меняет поведение сервиса)
server:
port: 8080 # порт сервиса в этом профиле (часть runtime-окружения)
# application-postgres.yml
# Конфигурация для режима с внешней БД (зависимость появляется "снаружи" сервиса)
app:
storage: postgres # режим хранения: ожидаем PostgreSQL и корректные настройки доступа
server:
port: 8080 # порт может совпадать, но условия запуска уже другие (нужна БД)
Здесь важны не конкретные имена свойств, а сама идея. Это один и тот же сервис, но условия его жизни различаются. В одном случае ему достаточно собственной JVM, в другом уже нужна база данных. Если это различие не собрано в голове, разработчик быстро начинает плодить «docker-версию проекта», «локальную специальную ветку» или отдельный набор непонятных конфигов. Это и есть начало runtime-хаоса.
Контейнеризация нужна здесь не для красоты. Она нужна, чтобы один и тот же сервис запускался в разных условиях через контролируемый runtime, а не через набор случайных практик. И это очень взрослое отличие между «сервис написан» и «сервисом можно нормально пользоваться».
4. Порты, пути и файлы ломают запуск легче всего 🚫
Есть соблазн думать, что настоящие боли начинаются только с больших зависимостей вроде базы или брокера. На практике всё часто ломается раньше и тише. Порт уже занят. Рабочая директория у процесса не та. Относительный путь к файлам считается не оттуда, откуда вы ожидали. На одной машине всё лежит в нужном месте, на другой — нет. И это особенно неприятно потому, что код при этом может быть совершенно корректным.
Даже такая простая настройка уже многое показывает:
server:
# Берём порт из переменной окружения SERVER_PORT, а если её нет — используем 8080
# Это делает "порт" явной частью runtime-окружения, а не неявной магией локальной машины
port: ${SERVER_PORT:8080}
Она напоминает о важной вещи: порт — это часть runtime, а не магическое число «по умолчанию». То же самое относится к файловым сценариям. В проекте Container-Ready Catalog Service есть экспорт каталога в файл, а значит, путь к этому экспорту — не нюанс, а полноценная часть жизненного цикла сервиса.
Если в коде живёт что-то вроде такого, то вы уже имеете дело с реальной средовой предпосылкой:
@Service
class ExportService {
Path defaultDir() {
// Относительный путь: считается от рабочей директории процесса (которая может отличаться)
// В контейнере это будет путь внутри файловой системы контейнера, если не примонтировать volume
return Path.of("./data/exports");
}
}
На одной машине этот путь выглядит безобидно, на другой приводит в неожиданную директорию, а в контейнерной среде вообще оказывается частью внутренней файловой системы процесса. Именно поэтому файлы и порты нельзя списывать на «мелочи». Это интерфейс между приложением и средой. 🔧
5. README помогает, но не гарантирует
Хороший README — это уважение к людям. Но у README есть честное ограничение: он остаётся текстом. Он может перечислить шаги, предупредить о зависимостях, напомнить про переменные среды. Но сам по себе не превращает запуск в воспроизводимую среду. Когда всё держится только на тексте, всегда остаётся зазор между «так должно быть» и «так реально получилось».
Это особенно хорошо видно на онбординге. Новый разработчик приходит в проект, клонирует репозиторий, читает инструкцию и вроде бы делает всё правильно. Но дальше всплывает привычная бытовая реальность: другая Java, другой локальный Postgres, другой свободный порт, другие права на каталог, другой shell, другая структура уже запущенных процессов. README помогает быстрее локализовать проблему, но саму проблему не устраняет.
Поэтому в инженерном смысле воспроизводимость — это всегда больше, чем документация. Документация нужна. Но если окружение не стабилизировано, проект всё равно остаётся проектом, который «как-то заводится у автора». Контейнеризация как раз делает следующий шаг: переносит часть знания о запуске из устных и текстовых договорённостей в более управляемый runtime-слой.
6. Что именно стабилизирует Docker 🛠️
Здесь особенно важно не впасть в магическое мышление. Docker не чинит плохой код. Он не исправляет неправильную бизнес-логику. Он не превращает непонятый Spring Boot startup в ясность. И он не заменяет вам знание профилей, конфигурации и runtime-поведения приложения. Если сервис падает из-за собственной ошибки, в контейнере он будет падать точно так же — иногда даже убедительнее.
Но Docker очень хорошо решает другой класс проблем. Он позволяет стабилизировать среду запуска, сделать её повторяемой и перестать тащить за собой бесконечный хвост неявных предпосылок. Проще говоря: он не лечит всё подряд, зато очень честно бьёт именно в ту боль, которая появляется после первого рабочего Boot-сервиса.
И вот мысль, которую полезно запомнить. Docker нужен не потому, что «так делают в индустрии». Он нужен потому, что после первого Spring Boot сервиса главной проблемой становится уже не только код, а воспроизводимый runtime. Как только эта мысль оседает, слова image, container, registry и Compose перестают быть сухим словарём и начинают работать по делу.
7. Линза воспроизводимости для любого Boot-сервиса
Ниже — небольшая таблица, которую удобно держать рядом всякий раз, когда вы собираетесь контейнеризовать уже существующий Spring Boot-сервис. Это не «список на экзамен», а рабочая линза. Если вы не можете ответить на эти вопросы, контейнеризация почти наверняка превратится в угадайку.
| Зона | Какой вопрос задать до контейнеризации | Что ломается, если вопрос не задан |
|---|---|---|
| Старт приложения | Что именно запускается и на каком порту? | Непонятно, что считать успешным стартом |
| Режимы работы | Какие профили или runtime-режимы есть у сервиса? | Контейнеризуется случайный локальный сценарий |
| Внешние зависимости | Нужны ли БД, кэш, брокер, файловая директория? | Запуск остаётся набором устных традиций |
| Файлы и пути | Куда сервис пишет и откуда читает? | На одной машине всё есть, на другой пусто |
| Операционный сигнал | Как быстро понять, что сервис действительно жив? | «Процесс стартовал» путается с «сервис готов» |
На нашем учебном проекте ответы на эти вопросы уже начинают проступать. Есть понятный старт, есть режимы standalone и postgres, есть экспорт в файл, есть business endpoint и есть Actuator. Именно поэтому starter repo здесь не декоративный. Он не просто показывает код — он показывает саму область задачи.
8. Типичные ошибки 🤦♂️
Ошибка №1: считать локальный запуск доказательством воспроизводимости.
Локальный успех говорит только о том, что ваша текущая машина совпала с ожиданиями проекта. Это полезно, но этого слишком мало. Как только меняется машина, разработчик или даже просто набор локально поднятых процессов, картина может резко поменяться.
Ошибка №2: воспринимать порт, путь или профиль как второстепенные детали.
Именно в таких «мелочах» часто и живёт большая часть боли. Сервис может быть идеально написан, но если у него не названы условия запуска, он остаётся хрупким. Runtime не начинается с базы данных; он начинается уже с порта и рабочей директории.
Ошибка №3: ожидать, что Docker сам исправит хаос конфигурации.
Контейнеризация стабилизирует среду, но не заменяет понимание приложения. Если вы не знаете, какие режимы существуют у сервиса и какие внешние предпосылки ему нужны, Docker просто сделает этот хаос чуть более упакованным.
Ошибка №4: подменять воспроизводимость хорошей инструкцией.
README нужен и важен, но текст не равен контролируемому runtime. Пока знание о запуске живёт только в документации и головах людей, проект остаётся уязвимым к любой разнице между машинами.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ