Прежде чем запустить контейнер, нужно собрать образ — упакованную копию вашего приложения со всем необходимым окружением. Образ описывается файлом Dockerfile. Разберём, как это устроено внутри и как написать Dockerfile для Spring Boot приложения с нуля.
Проблема: «у меня работает, а на сервере нет»
Представьте: вы написали сервис, запустили его на своём ноутбуке — всё хорошо. Отправили на сервер — упало. Оказалось, что на ноутбуке стоит Java 21, а на сервере — Java 17. Или какая-то нативная библиотека установлена локально, но не на целевой машине. Или конфигурационный файл лежит не там.
Это классическая проблема «работает у меня». Решение — упаковать приложение вместе с рантаймом и всеми зависимостями в единый образ. Тогда везде, где запущен Docker, поведение будет одинаковым: на ноутбуке, в CI, на сервере.
Образ — это неизменяемый шаблон, из которого создаются контейнеры. Один образ — сколько угодно контейнеров на его основе.
Что такое образ и слои
Образ Docker — это не просто архив с файлами. Внутри он состоит из слоёв (layers), уложенных один поверх другого.
Каждый слой — результат одной инструкции в Dockerfile. Слой фиксирует изменения файловой системы: добавленные файлы, установленные пакеты, изменённые настройки. Финальный образ — это стопка слоёв, которые Docker склеивает в единую файловую систему.
Короткая формула: образ = базовый образ + набор слоёв, каждый из которых соответствует одной инструкции Dockerfile.
Зачем слои? Для переиспользования кэша. Если вы поменяли только код приложения, Docker не будет повторно скачивать базовый образ и переустанавливать зависимости — эти слои уже есть локально. Перестраивается только то, что изменилось, и всё, что идёт после. Это значительно ускоряет повторные сборки.
[ eclipse-temurin:21-jre ] ← базовый образ (уже готовый)
+
[ WORKDIR /app ] ← слой 1
+
[ COPY app.jar app.jar ] ← слой 2
+
[ ENTRYPOINT [...] ] ← слой 3
=
ваш образ
Dockerfile — инструкция по сборке
Dockerfile — текстовый файл с набором инструкций. Docker читает его сверху вниз и выполняет каждую инструкцию, создавая очередной слой.
Разберём основные инструкции на реальном примере.
FROM — базовый образ
FROM eclipse-temurin:21-jre
FROM — первая и обязательная инструкция. Она говорит Docker, от какого образа отталкиваться. eclipse-temurin:21-jre — это официальный образ с JRE 21 (только runtime, без JDK и компилятора). Для запуска Spring Boot jar-файла этого достаточно.
21-jre — это тег: версия образа. Всегда указывайте конкретный тег, а не latest — иначе сборка может сломаться при выходе новой версии.
WORKDIR — рабочая директория
WORKDIR /app
WORKDIR задаёт директорию внутри контейнера, в которой будут выполняться последующие инструкции (COPY, RUN, CMD). Если директории нет — она создаётся автоматически. Это как сделать cd /app, только для Docker.
COPY — копирование файлов
COPY build/libs/app.jar app.jar
COPY копирует файлы из вашей локальной файловой системы (слева) внутрь образа (справа). Здесь мы копируем собранный jar в рабочую директорию контейнера под именем app.jar.
Первый аргумент — путь относительно контекста сборки (обычно это директория, откуда вы запускаете docker build). Второй — путь внутри образа.
RUN — выполнение команд при сборке
RUN apt-get update && apt-get install -y curl
RUN выполняет команду во время сборки образа и сохраняет результат как новый слой. Используется для установки пакетов, создания директорий, любой подготовки окружения.
Несколько команд лучше объединять через && в одну инструкцию RUN — это создаёт меньше слоёв и не оставляет промежуточных кэшей в образе.
ENV — переменные окружения
ENV JAVA_OPTS="-Xmx512m -Xms256m"
ENV задаёт переменные окружения внутри образа. Они доступны как при сборке, так и при запуске контейнера. Удобно для настройки JVM или передачи конфигурации приложению.
Переменные, которые должны меняться при запуске (пароли, URL баз данных), лучше передавать через docker run -e или docker compose — не зашивать в образ.
EXPOSE — документирование порта
EXPOSE 8080
EXPOSE объявляет, на каком порту слушает приложение внутри контейнера. Это только документация — реальный проброс портов настраивается при запуске контейнера (docker run -p). Но EXPOSE помогает читателю Dockerfile понять, как с приложением работать.
CMD и ENTRYPOINT — запуск приложения
Это самые часто путаемые инструкции. Обе задают, что выполнить при старте контейнера, но по-разному.
CMD — команда по умолчанию. Её можно заменить при запуске контейнера, передав другую команду в docker run. Например:
CMD ["java", "-jar", "app.jar"]
ENTRYPOINT — фиксированная точка входа. Команда из ENTRYPOINT всегда выполняется; то, что передаётся через docker run, добавляется к ней как аргументы, а не заменяет её. Пример:
ENTRYPOINT ["java", "-jar", "app.jar"]
Самый распространённый приём — комбинация: ENTRYPOINT фиксирует исполняемый файл, CMD даёт аргументы по умолчанию (которые можно переопределить):
ENTRYPOINT ["java"]
CMD ["-jar", "app.jar"]
Для простых случаев достаточно одного ENTRYPOINT. Используйте exec-форму (массив JSON, как выше) — она запускает процесс напрямую, без промежуточного shell, что важно для корректной обработки сигналов остановки контейнера.
Полный Dockerfile для Spring Boot
Собираем всё вместе:
FROM eclipse-temurin:21-jre
WORKDIR /app
# копируем собранный jar (предполагается: ./gradlew bootJar уже выполнен)
COPY build/libs/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Это минимальный рабочий Dockerfile. Он берёт готовый jar, помещает его в образ и запускает при старте контейнера.
Сборка образа: docker build
Собрать образ из Dockerfile можно командой:
# собрать образ и присвоить тег my-app:1.0
docker build -t my-app:1.0 .
Флаг -t (от tag) задаёт имя и тег образа. Точка в конце — это контекст сборки: директория, содержимое которой Docker может использовать в инструкциях COPY. Обычно это текущая директория проекта.
Посмотреть созданный образ:
docker images
После сборки образ можно запустить:
docker run -p 8080:8080 my-app:1.0
Ключ -p 8080:8080 пробрасывает порт: <порт на хосте>:<порт в контейнере>.
Кэш слоёв и порядок инструкций
Docker кэширует слои. Если при повторной сборке слой не изменился — он берётся из кэша, а не перестраивается. Но как только один слой изменился — все последующие пересобираются.
Из этого следует важное правило: инструкции, которые меняются редко, ставьте выше; то, что меняется часто — ниже.
В Spring Boot приложении jar-файл меняется при каждой сборке, поэтому COPY app.jar стоит в конце. Если бы до него шла установка зависимостей через RUN, этот слой также пришлось бы пересобирать при каждом изменении кода. Правильный порядок экономит минуты на каждой итерации.
Для ещё более эффективного кэширования (разделение зависимостей и кода приложения) используют многоэтапную сборку — это отдельная тема, разобранная в статье про слои и многоэтапную сборку.
Коротко
- Образ — неизменяемый шаблон для запуска контейнеров; состоит из слоёв.
- Каждый слой = одна инструкция
Dockerfile; неизменённые слои берутся из кэша при повторной сборке. FROMзадаёт базовый образ;eclipse-temurin:21-jre— стандартный выбор для Java 21.COPYкопирует файлы,RUNвыполняет команды при сборке,ENVзадаёт переменные окружения.ENTRYPOINTфиксирует точку входа;CMDдаёт аргументы по умолчанию (можно переопределить при запуске).docker build -t имя:тег .собирает образ;.— контекст сборки.- Редко меняющиеся инструкции ставьте выше по
Dockerfile— так кэш используется эффективнее.
Что почитать дальше
- Что такое Docker и зачем он нужен — если ещё не знакомы с базовыми концепциями контейнеризации.
- Контейнеризация Spring Boot приложения — пошаговый разбор от сборки jar до запуска в контейнере.
- Многоэтапная сборка и оптимизация слоёв — как уменьшить размер образа и ускорить сборку с multi-stage builds.