Всем привет! В данной статье хочу показать простой пример написания Dockerfile, объяснить как это все работает, а также показать на примере использование многоэтапной сборки.
Для понимания данной статьи необходимы минимальные знания Docker, а также для использования примеров - установленный Docker Desktop локально на компьютере.
Вначале немного теории.
Dockerfile - это файл, который содержит инструкции для сборки образа. На основании образа создается и запускается контейнер.
Обратимся к официальной документации https://docs.docker.com/get-started/overview/
Это скриншот взят с официальной страницы.
На этом скриншоте мы видим схему как создаются и запускаются контейнеры. Мы как клиенты через команды docker обращаемся к Docker daemon, который берет локальный образ (image) и на основании его запускает контейнер, если образа локально нет - то он идет в registry (это может быть docker hub) и вначале стягивает его себе на компьютер. И может кто-то спросит "А где тут во всем этим dockerfile?". Именно на основании dockerfile и создается первоначально образ.
Для тестирования написания dockerfile создадим простой проект, который по сути ничего особо делать не будет, просто будет приветствовать нас.
Зайдем на https://start.spring.io/ и создадим проект назовем его simple-dockerfile-example. Из зависимостей подключим только Spring Web.
Генерируем и открываем этот проект в IDEA.
Создаем класс ClientService:
@Service
public class ClientService {
@Value("${client}")
private String client;
public String sayHello(){
return "Hello!: " + client;
}
}
и класс ClientController:
@RestController
@RequestMapping("api/v1/client")
public class ClientController {
private final ClientService clientService;
@Autowired
public ClientController(ClientService clientService) {
this.clientService = clientService;
}
@GetMapping()
public String sayHello(){
return clientService.sayHello();
}
}
Также в application.properties пропишем одно свойство (client), то есть имя того кого будем приветствовать.
client=Ivan
Можем протестировать наш код, запустить проект и через postman отправить get запрос на http://localhost:8080/api/v1/client
Должны получить приветствие.
Сейчас попробуем на основании нашего проекта сделать образ и его запустить в контейнере. Для этого в корне нашего проекта создадим файл Dockerfile:
и в данном файле пишем:
FROM eclipse-temurin
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
Разберем что тут происходит в данном файле.
В первой строке мы указываем какой образ нужно стянуть с docker hub.
Этот образ нужен для того чтобы в контейнере была развернута своя JDK для запуска нашего проекта.
ARG JAR_FILE=target/*.jar - здесь мы создаем просто локальную переменную, которая ссылается на jar-ник нашего проекта.
COPY ${JAR_FILE} app.jar - мы копируем наш jar-ник в образ и называем его app.jar.
EXPOSE 8080 - указываем на каком внешнем порту будет доступен наш контейнер.
ENTRYPOINT ["java", "-jar", "/app.jar"] - мы запускаем наш jar-ник.
Если кому-то не очень понятно что происходит в последней строчке, то это то же самое, что если бы мы предварительно запустили команду package и получили simple-dockerfile-example-0.0.1-SNAPSHOT.jar в папке target.
А затем с помощью командной строки зашли в папку target и выполним команду java -jar simple-dockerfile-example-0.0.1-SNAPSHOT.jar
И мы таким образом запустили наш проект. Можем сейчас зайти в postman и отправить get запрос на http://localhost:8080/api/v1/client и получить снова Hello!: Ivan.
Останавливаем проект. Заходим в терминале в корень нашего проекта, где лежит Dockerfile и сейчас будем строить наш образ. Выполним команду docker build .
Docker daemon должен стянуть всё необходимое с docker hub. Наш образ готов.
Посмотреть наши образы можно через команду docker images в командной строке или использовать Docker Desktop. Находим его во вкладке Images:
Мы видим, что образ есть и он занимает 490,84 МВ. Также выполним команду docker images в командной строке.
Дальше давайте запустим наш образ, то есть поместим его в контейнер.
Выполним команду docker run -p 8080:8080 b472
-p в данной команде мы указываем порты. Первый порт -это порт на нашем компьютере то есть наш localhost, а второй порт - это порт внутри контейнера.
b472 - это первые 4 буквы ID нашего контейнера, у Вас они будут отличаться.
После выполнения данной команды контейнер должен запуститься с нашим проектом и отправив через postman get запрос на http://localhost:8080/api/v1/client мы снова должны получить Hello!: Ivan.
Таким образом мы создали проект, создали образ с данным проектом и развернули его через docker в контейнере.
Что плохо написано в нашем Dockerfile.
Мы не указали версию JDK (это первая строка FROM eclipse-temurin), если версия не указана, то будет скачана самая последняя доступная версия, что может привести к несовместимости нашего приложения с JDK. И во-вторых здесь мы не компилируем jar, а только его запускаем, поэтому нам бы хватило и JRE и это уменьшит размер нашего образа. Переделаем это.
FROM eclipse-temurin:17-jre-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
Остановим наш контейнер через Docker Desktop и запустим снова команду docker build.
Выполним команду docker images:
И мы увидим, что размер нового образа 188 МВ в 2,5 раза меньше, чем предыдущий, а работает все также.
Перейдем сейчас к использованию многоэтапной сборки. Суть ее в том, что как вначале чтобы сделать образ мы сами вручную запускали команду мавена и создавали jar-ник нашего проекта, а при многоэтапной сборке jar-ник будет генерироваться автоматически.
Перепишем dockerfile.
FROM eclipse-temurin:17-jdk-alpine as builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install
FROM eclipse-temurin:17-jre-alpine
WORKDIR /opt/app
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
Разберемся что тут происходит.
FROM eclipse-temurin:17-jdk-alpine as builder - здесь, как мы уже знаем, указываем docker-у какой образ мы будем использовать для сборки нашего проекта и тут мы уже указываем именно jdk, так как нам понадобятся инструменты для компиляции нашего проекта. as builder - это название, которое мы присвоили, для того чтобы обратиться с другого слоя контейнера для получения данных.
WORKDIR /opt/app - создаем папки в данном слое
COPY .mvn/ .mvn - копируем папку mvn и все ее содержимое в такую же папку в корень данного слоя, для того чтобы у нас был maven.
COPY mvnw pom.xml ./ - копируем mvnw и pom.xml тоже в корень данного слоя.
RUN ./mvnw dependency:go-offline - данной строчкой мы подтягиваем все зависимости из pom.xml в наш слой, чтобы у нас в контейнере были все зависимости, необходимые для нашего проекта.
COPY ./src ./src - копируем непосредственно папку с нашим проектом.
RUN ./mvnw clean install - запускаем мавен, который все чистит и создает jar-ник нашего проекта.
строчка 10 WORKDIR /opt/app - создаем папки в другом слое.
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar - используя доступ к первому слою копируем jar-ник с нашим проектом в данный слой.
Давайте сейчас запустим все это дело.
Вначале остановим и удалим предыдущий запущенный контейнер через Docker Desktop.
Запустим снова команду docker build .
Немного подождем пока maven подтянет все зависимости.
Запустим команду docker images и посмотрим, что у нас появился новый образ.
и запустим его командой docker run -p 8080:8080 86ed
Контейнер запустился тестируем.
Еще хочу показать одну вещь. На основании одного образа можно запустить несколько контейнеров на разных портах и с разными свойствами.
Открываем например еще одну командную строку, я буду использовать PowerShell. Заходим в наш каталог, где лежит dockerfile и выполняем команду: docker run -p 7070:8080 -e client=test 86ed. Здесь мы запускаем контейнер с доступом на порту 7070 и с переменной client=test.
Заходим в postman.
И мы видим что мы изменили переменную с Ivan на test.
Запустим еще один контейнер с параметром develop на порту 9090.
Тестим.
Заходим в Docker Desktop и можем посмотреть, что все наши контейнеры работают, но работают на разных портах и с разными параметрами.
Это нужно для того, чтобы, например, развернуть один и тот же проект, но один для тестирования с подключенной тестовой базой данных, а один с рабочей.
Спасибо Всем кто дочитал до конца.