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

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

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

Комментарии (5)


  1. SimSonic
    01.02.2023 18:49

    Чем eclipse-temurin лучше, чем, например, bellsoft/liberica-jdk-alpine-musl? Последний видимо будет ещё меньше в размере.

    Вариант с копированием такого банального инструмента, как мавен, имхо, следовало бы дожать до сборки своего тулинг-образа, или хотя бы поиска подходящего в интернете.


  1. MountainGoat
    02.02.2023 05:53

    Заголовок про Dockerfile, статья только про Spring. С тем же успехом могли назвать статью "Как программировать на Java". Не надо так делать.


    1. lllamnyp
      02.02.2023 06:28

      Я тоже усмехнулся, мол, автор нашёл пример "попроще", чтобы знакомиться с докером. Но ничего, дожал до конца, показал нюансы с jdk и jre. Можно ведь и в другую крайность податься:

      1. Берете ваш статически-линкованный бинарь (откуда он взялся, оставим за скобками).

      2. FROM scratch COPY ./app /app ENTRYPOINT ["/app"]

      3. docker build, docker run

      4. ???

      5. PROFIT!!1


      1. max_im_ka
        03.02.2023 02:09

        Ага и минус пол статьи) потом скажут чего так мало))


  1. DonAlPAtino
    02.02.2023 17:15

    А есть какой-нибудь образ для сборки меньше чем

    maven 3.6.0-jdk-11-slim c7428be691f8 3 years ago 489MB

    А то как-то 500 мегов, да еще и все библиотеки тянет.