1. Базовые концепции Docker: образы, контейнеры и синтаксис Dockerfile
Представьте классическую ситуацию из жизни разработчика: вы написали отличный микросервис на Spring Boot, протестировали его локально, и всё работает идеально. Вы передаёте скомпилированный .jar файл инженеру по тестированию или DevOps-специалисту для развёртывания на сервере. Через десять минут вам пишут: «Приложение падает при запуске».
Вы начинаете разбираться и выясняете, что на сервере установлена Java 11, а ваш код использует новые возможности Java 17. Кроме того, на сервере другие пути к файлам конфигурации и отличается версия операционной системы. Возникает знаменитая проблема: «На моей машине всё работает!».
Чтобы навсегда забыть об этой фразе, индустрия пришла к контейнеризации. Это технология, которая позволяет упаковать приложение со всем его окружением (библиотеками, настройками, версией языка) в единый стандартизированный блок. Самым популярным инструментом для этого сегодня является Docker.
Метафора морских перевозок
Понять суть Docker проще всего через историю грузоперевозок. До 1956 года товары перевозили в бочках, ящиках и мешках разных размеров. Погрузка корабля занимала дни: грузчикам приходилось думать, как безопасно разместить хрупкие ящики рядом с тяжелыми бочками. Перенос груза с корабля на поезд требовал повторной ручной перевалки каждой единицы.
Всё изменилось с изобретением стандартизированного морского контейнера. Теперь неважно, что внутри — автомобильные запчасти, электроника или бананы. Контейнер имеет стандартные размеры, стандартные крепления и одинаково легко ставится на корабль, грузовик или поезд.
> Docker делает для программного обеспечения то же самое, что морской контейнер сделал для мировой логистики. Он стандартизирует процесс доставки кода от ноутбука разработчика до серверов в дата-центре.
Контейнеры против виртуальных машин
До появления Docker проблему изоляции приложений решали с помощью виртуальных машин (VM). Виртуальная машина эмулирует полноценный физический компьютер. Если вам нужно запустить три изолированных приложения, вы создаете три виртуальные машины, на каждую устанавливаете полноценную гостевую операционную систему (например, Ubuntu или Windows), выделяете ей фиксированную оперативную память и процессорное время.
Это надежно, но крайне ресурсоемко. Каждая гостевая ОС потребляет гигабайты памяти и долго загружается.
Docker использует другой подход — виртуализацию на уровне операционной системы. Контейнеры не содержат собственной ОС. Они используют ядро операционной системы хоста (сервера, на котором запущены), но при этом изолированы друг от друга на уровне процессов и файловых систем.
| Характеристика | Виртуальная машина (VM) | Docker-контейнер | | :--- | :--- | :--- | | Изоляция | Полная (аппаратный уровень) | Частичная (уровень процессов ОС) | | Размер | Гигабайты (включает ядро и ОС) | Мегабайты (только приложение и библиотеки) | | Время запуска | Минуты (загрузка ОС) | Секунды или миллисекунды | | Утилизация ресурсов | Выделяются жестко заранее | Динамически распределяются хостом |
Образы и контейнеры: классы и объекты
В основе работы Docker лежат два фундаментальных понятия, которые Java-разработчику будет очень легко понять через аналогию с объектно-ориентированным программированием.
Docker Image (Образ) — это неизменяемый шаблон, содержащий файловую систему, код вашего приложения, среду выполнения (например, JRE), библиотеки и переменные окружения. Образ можно сравнить с классом в Java. Это просто чертеж, инструкция. Образ не выполняется сам по себе, он хранится на диске.
Docker Container (Контейнер) — это запущенный экземпляр образа. Если образ — это класс, то контейнер — это объект (экземпляр класса), созданный в оперативной памяти. Из одного образа (класса) можно запустить сотни идентичных контейнеров (объектов).
Важное свойство контейнеров — их эфемерность. Контейнеры созданы для того, чтобы их можно было легко остановить, удалить и пересоздать заново. Любые данные, которые контейнер сохраняет внутри себя во время работы, исчезнут при его удалении.
Синтаксис Dockerfile: пишем рецепт
Чтобы создать собственный образ, нужен Dockerfile — обычный текстовый файл с набором инструкций. Docker читает эти инструкции сверху вниз и шаг за шагом собирает образ.
Рассмотрим типичный Dockerfile для микросервиса на Spring Boot:
Разберем каждую инструкцию детально:
FROM — базовый образ. Сборка всегда начинается с фундамента. В нашем случае мы берем готовый образ eclipse-temurin:17-jre-alpine, который уже содержит Java 17 (JRE). Приставка alpine означает, что в основе лежит минималистичный дистрибутив Linux Alpine, размер которого всего около 5 мегабайт.WORKDIR — устанавливает рабочую директорию внутри будущего образа. Все последующие команды будут выполняться в папке /app.COPY — копирует файлы с вашего компьютера (хоста) внутрь образа. Здесь мы берем собранный Maven'ом файл target/payment-service-1.0.0.jar и кладем его в образ под коротким именем app.jar.EXPOSE — документационная инструкция. Она не открывает порты физически, а лишь сообщает разработчикам и системам оркестрации, что приложение внутри контейнера будет слушать порт 8080.ENTRYPOINT — главная команда, которая будет выполнена при старте контейнера. Именно она запускает наше Java-приложение.Слоистая архитектура (Layers)
Каждая инструкция в Dockerfile (особенно RUN, COPY, ADD) создает новый слой (layer) в образе. Docker кэширует эти слои.
Представьте, что вы изменили одну строчку кода в Java-классе и пересобрали .jar файл. При новой сборке Docker-образа система увидит, что базовый образ (FROM) и рабочая директория (WORKDIR) не изменились, и мгновенно возьмет их из кэша. Изменится только слой COPY (так как хэш-сумма нового .jar файла другая) и все слои после него. Это делает повторные сборки невероятно быстрыми.
Multi-stage сборка: оптимизация для Java
В примере выше мы копировали уже готовый .jar файл. Это значит, что перед запуском Docker вам нужно локально выполнить mvn clean package. Но что если у разработчика на компьютере вообще не установлен Maven или нужная версия Java?
Правильный подход в CI/CD — поручить сборку самого кода тоже Docker'у. Для этого используется Multi-stage build (многоэтапная сборка).
В этом рецепте мы используем два базовых образа:
На первом этапе (AS builder) мы берем тяжеловесный образ с установленным Maven и JDK (около 400 МБ). Копируем туда исходный код и запускаем сборку.
На втором этапе мы берем легкий образ только с JRE (около 50 МБ). Ключевая магия происходит в строке COPY --from=builder. Мы забираем готовый .jar файл из первого этапа и кладем в финальный образ.
В результате тяжелый образ с Maven и исходным кодом выбрасывается. Финальный образ получается компактным, безопасным (в нем нет исходников) и содержит только то, что необходимо для работы приложения.
Жизненный цикл: от сборки до запуска
Когда Dockerfile готов, мы используем терминал для управления процессом.
Сначала мы просим Docker прочитать Dockerfile и создать образ:
docker build -t my-spring-app:1.0 .
Флаг -t (tag) задает имя и версию образа. Точка в конце указывает, что Dockerfile нужно искать в текущей директории.
После успешной сборки мы запускаем контейнер:
docker run -d -p 8080:8080 my-spring-app:1.0
Здесь появляются новые флаги:
-d (detach) запускает контейнер в фоновом режиме, освобождая терминал.-p 8080:8080 (publish) связывает порты. Левая часть — это порт на вашей физической машине (хосте), правая — порт внутри контейнера. Если ваше Spring-приложение работает на порту 8080 внутри изолированного контейнера, без этого флага вы не сможете достучаться до него из браузера.Теперь ваше приложение надежно упаковано. Этот образ можно отправить в специальное хранилище (Docker Registry), откуда сервер скачает его и запустит точно так же, как вы сделали это на своем ноутбуке. Окружение стало частью самого приложения, и проблема «на моей машине работает» решена навсегда.