Простой пример написания Dockerfile. Использование многоэтапной сборки

от автора

Всем привет! В данной статье хочу показать простой пример написания 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 и можем посмотреть, что все наши контейнеры работают, но работают на разных портах и с разными параметрами.

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

Спасибо Всем кто дочитал до конца.


ссылка на оригинал статьи https://habr.com/ru/post/713942/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *