Docker Java основные команды. Инструкция для начинающих.
Эта статья посвящена основам Docker и раскрывает азы работы с контейнерами. Мы изучим базовые определения и самые необходимые команды и даже разработаем и развернём простейшее Java-приложение.
@javatg – лучшие материалы Java
Что такое Docker
Docker — инструмент, предназначенный для быстрой разработки, доставки и развёртывания приложений. Он позволяет упаковать приложение вместе со всеми его зависимостями в так называемый контейнер, а затем запустить его в любой среде.
Идея контейнеризации состоит в том, что на одной машине может разворачиваться множество таких контейнеров с приложениями. Для каждого из них в операционной системе выделяется изолированная область — осуществляется виртуализация на уровне ОС.
Важный момент: все контейнеры запускаются одинаковым способом вне зависимости от того, что находится внутри. Это напоминает контейнеры для морских перевозок — с виду они одинаковы, но внутри могут храниться совершенно разные грузы.
Преимущества контейнеров:
- приложения получают единый механизм сборки;
- не нужно конфигурировать среду для запуска — она поставляется вместе с приложением;
- приложения легче масштабировать;
- есть система оркестрации контейнеров, позволяющая ими управлять.
Основные понятия
Образ — некий шаблон, на основе которого создаются контейнеры. Содержит всё необходимое для запуска приложения. Сюда относятся код, системные утилиты, библиотеки, настройки и так далее. Образ можно представить в виде набора слоёв, которые накладываются друг на друга. Каждый последующий добавляет, изменяет или удаляет файлы предыдущего слоя.
DockerfIle — текстовый файл с набором инструкций по созданию образа, каждая из которых добавляет к образу новый слой.
Контейнер — конкретный экземпляр приложения, созданный на основе образа. Причём из одного образа можно создать сколько угодно контейнеров. Технически контейнер создаётся путём добавления к образу нового слоя, содержащего результаты работы приложения.
Реестр — хранилище образов (как GitHub для кода приложений). Образы можно скачивать из реестра и создавать на их основе контейнеры. Также в реестр можно загружать новые или изменённые образы для дальнейшего использования.
Пример использования
Давайте разработаем простое Spring Boot приложение, создадим на его основе образ и развернём контейнер на локальной машине. Это делается в три простых шага:
- Устанавливаем Docker. Все инструкции можно найти на официальном сайте.
- Генерируем проект с помощью конструктора Spring. Задаём spring-docker-simple в полях Artifact и Name.
- Выбираем одну зависимость — Spring Web. Готово! Скачиваем и распаковываем архив, открываем его в среде разработки.
Код приложения
В открывшемся проекте рядом с главным классом, содержащим метод main, создаём ещё один класс — контроллер с методом hello ().import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloDockerController { @GetMapping("/") public String hello() { return "Hello Docker!"; } }
Класс помечен аннотацией @RestController, означающей, что он предназначен для обработки web-запросов. А метод помечен аннотацией @GetMapping c адресом «/» — перейдя по нему (выполнив get-запрос к http://localhost:8080/), мы получим сообщение «Hello Docker!»
Теперь открываем терминал и вводим команду:./mvnw package && java -jar target/spring-docker-simple-0.0.1-SNAPSHOT.jar
Она упакует приложение в jar-файл и запустит его. Чтобы убедиться в корректности работы приложения — перейдём на http://localhost:8080/ в браузере и увидим заветное сообщение.
Теперь создаём файл с именем Dockerfile в корне проекта, который содержит инструкции для сборки образа со следующим текстом:FROM adoptopenjdk/openjdk11:alpine-jre ARG JAR_FILE=target/spring-docker-simple-0.0.1-SNAPSHOT.jar WORKDIR /opt/app COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","app.jar"]
Вот что происходит, когда мы вводим этот код:
Команда | Описание |
---|---|
FROM adoptopenjdk/openjdk11:alpine-jre | Oбраз создаётся на основе alpine linux с установленной openjdk11 |
ARG JAR_FILE=target/spring-docker-simple-0.0.1-SNAPSHOT.jar | Переменной JAR_FILE присваивается путь к jar- архиву |
WORKDIR /opt/app | Назначаем рабочую директорию, в которой будут выполняться дальнейшие команды (перемещаемся в папку app) |
COPY ${JAR_FILE} app.jar | Наш jar-файл, указанный в JAR_FILE, копируется в папку app, и копии задаётся имя app.jar |
ENTRYPOINT [“java”,”-jar”,”app.jar”] | jar-файл запускается, собирается команда java -jar app.jar из заданной рабочей директории |
После этого в терминале вводим команду, с помощью которой собираем образ и запускаем контейнер.docker build -t spring-docker-simple:0.0.1 .
Точка в конце важна, она указывает на расположение Dockerfile (символ «точка» означает текущую директорию. Проверьте, что образ создан командой docker images. Вывод должен быть таким:
Запускаем контейнер командой:docker run -d -p 8080:8080 -t spring-docker-simple:0.0.1
Опция -d означает старт процесса в фоновом режиме. Опция -p тоже важна — дело в том, что контейнер собирается в полностью изолированном окружении. Тот факт, что приложение внутри контейнера запущено на порту 8080, не означает, что оно доступно вне контейнера на этом порту.
Требуется явно указать, что порту 8080 в контейнере (здесь второе значение — это порт, на котором работает наше приложение в контейнере) соответствует порт 8080 на локальной машине, который будет использоваться при обращении к контейнеру. Поэтому пишем через двоеточие -p 8080:8080.
Теперь введём в терминале команду:curl http://localhost:8080
Так проверяется работоспособность запущенного контейнера. Есть и альтернативный вариант: можно просто перейти по этому адресу в браузере. Если всё работает как надо, задача выполнена — нам удалось упаковать Spring Boot приложение в контейнер.
Частые команды при работе с Docker
docker ps — выводит список запущенных контейнеров. Также ей можно передать параметр -a, чтобы вывести все контейнеры, а не только запущенные.
docker build — собирает образ Docker из Dockerfile и набора файлов, расположенных по определённому пути.
Параметр -t используется, чтобы задать имя образа, последний параметр. — наименование каталога (в нашем случае текущий каталог).
docker images — выводит список образов в вашей системе.
docker logs — позволяет вывести на консоль логи указанного контейнера. Для этого необходимо указать имя или id контейнера. Можно использовать флаг –follow, чтобы следить за логами работающего контейнера: например, docker logs –follow c5ecc88de8f9.
docker run — запускает контейнер на основе указанного образа.
docker stop — останавливает контейнер. Можно передать опцию $(docker ps -a -q) для остановки всех запущенных контейнеров.
docker rm и docker rmi — команды, удаляющие контейнер и образ соответственно.
Удалить все контейнеры:
Контейнеры создаются на основе image (образа): то есть, это определенный шаблон в котором написано все необходимое для создания докер контейнера. Как создать этот образ? В нашем случае нам нужно будет создать файл Dockerfile в корне проекта с описанием того, что должно быть в контейнере. Так как мы не хотим где-то показывать токен бота, придется извернуться и передавать его каждый раз, когда мы захотим развертывать приложение. Более детально об этой теме почитать можно здесь и здесь.
Пишем JRTB-13
Нужно настроить быстрый и легкий процесс развертывания (деплоя) нашего приложения на сервер. То есть на машину, которая работает 24/7. За основу возьмем докер. Но задачи в нашем списке, которая бы отвечала за добавление этой функциональности, нет. Как-то я его пропустил при создании. Ничего страшного, сейчас создадим. Заходим на вкладку создания issue на гитхаб и выбираем Feature Request:
Добавляем описание задачи, критерии его приемки, устанавливаем, к какому проекту этот issue относится и можно создавать новое issue:
Теперь чтобы показать, что задача взята в работу, сменим статус задачи с To do на In Progress:
Это будет сложная статья. Если будут проблемы — пишите в комментариях: я буду следить и отвечать на них в меру сил. Такой будет небольшой Customer Support 😀
Создаем Dockerfile
Что такое докерфайл? Для докера это скрипт (пошаговая инструкция), как создавать образ для докер контейнера. Для работы нашего приложения нужна JDK, причем 11-й версии. То есть, нам нужно найти докер-образ JDK 11 и добавить его в наш образ. Это что-то сродни с тем, как мы добавляем зависимость в помник. Для этого дела у докера есть DockerHub. Чтобы локально загружать образы, нужно там зарегистрироваться. После регистрации идем искать нам JDK11. Из того, что получилось найти — вот этот контейнер: adoptopenjdk/openjdk11. В описании этого контейнера есть то, что нужно для докерфайла:
FROM adoptopenjdk/openjdk11:ubi
RUN mkdir /opt/app
COPY japp.jar /opt/app
CMD ["java", "-jar", "/opt/app/japp.jar"]
Поправим папку, из которой мы берем jar файл. У нас он находится в target папке после того, как мы запускаем mvn package задачу мавена. Перед тем, как все это делать, на основе обновленной main ветки создаем новую, для нашей задачи: STEP_4_JRTB-13. Теперь можно работать. В корне проекта создаем файл без расширения Dockerfile и добавим внутрь следующее:
FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Первая строка — на основе чего будет состоять образ — adoptopenjdk/openjdk11. Вторая строчка — добавляем аргумент в образ с именем JAR_FILE, который находится в папке target. Причем нынешняя папка определяется по месту Dockerfile. Третья строка — копируем в докер-образ jar нашего проекта. Последняя строка по сути содержит массив, созданный из команды в терминале, которую разделили по пробелу. То есть, в итоге будет выполнено следующее: “java -jar /app.jar” Чтобы держать в тайне токен бота, при запуске контейнера нам нужно будет передавать две переменные — имя бота и его токен. Для этого напишем запрос, который должен запустить наш проект с переменными. А как это сделать? Нужно загуглить: вот первая ссылка с нормальным описанием. А что мы хотим сделать? У нас в application.properties файле есть две переменные, которые мы там определяем:
- bot.username
- bot.token
Я хочу запускать докер контейнер и каждый раз передавать туда свое значение, чтобы никто не видел эти значения. Я знаю, что в SpringBoot переменные окружения, которые задаются в момент запуска jar проекта, будут более приоритетнее чем те, которые находятся в файле application.properties. Чтобы передать переменную в запросе, нужно добавить следующую конструкцию: -D{имя переменной}=”{значение переменной}”. Фигурные скобки не дописываем 😉 Получим запрос, при котором будет запущено наше приложение с предопределенными значениями — имя и токена бота: java -jar -Dbot.username=”test.javarush.community_bot” -Dbot.token=”dfgkdjfglkdjfglkdjfgk” *.jar Теперь нужно передать эти переменные внутрь докер контейнера. Это environment variable. Чтобы в будущем у нас база данных работала четко и без проблем с нашим приложением, будем использовать docker-compose. Это отдельный инструмент, в котором можно упорядочить работу, запуск и зависимости между контейнерами. Иными словами, это надстройка над докером, чтобы управлять контейнерами одной инфраструктуры. Плюс перед тем, как запустить docker-compose, нужно быть уверенным, что мы стянули все изменения кода с сервера, собрали приложение и остановили старую версию. Для этого будем использовать баш скрипт. Ух… Звучит все непросто, согласен. Но работа с настройкой развертывания приложений — это всегда муторный и сложный процесс. Поэтому у нас вырисовывается нехилая схема:
- Запускаем баш скрипт.
- Баш скрипт запускает docker-compose.
- Docker-compose запускает docker контейнер с нашим приложением.
- Docker контейнер запускает наше приложение.
И вот нужно сделать так, чтобы две переменные — имя бота и его токен — прошли из 1 пункта в 4. Причем так, чтобы эти две переменные использовались при запуске нашего java-приложения. Пойдем с конца в начало. Мы уже знаем, какую команду нужно выполнить, чтобы запустить джарник. Поэтому будем настраивать Dockerfile, чтобы он научился принимать две переменные и передавать их в запрос. Для этого приведем Dockerfile к следующему виду:
FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
ENV BOT_NAME=test.javarush_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Dbot.username=${BOT_NAME}", "-Dbot.token=${BOT_TOKEN}", "-jar", "/app.jar"]
Видно, что мы добавили две строки и обновил ENTRYPOINT. Строки:
ENV BOT_NAME=test.javarush_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
объявляют переменные внутри кодер файла. По умолчанию у них значение указано. Если при создании образа из этого докерфайла будут переданы переменные окружения с такими именами, значения будут другие. А в ENTRYPOINT мы добавили еще несколько элементов, которые будут считывать эти переменные среды:
"-Dbot.username=${BOT_NAME}", "-Dbot.token=${BOT_TOKEN}"
Здесь видно, что внутри строки при помощи ${} конструкции будут переданы значения BOT_NAME и BOT_TOKEN. Далее нам нужно научить получать и передавать эти переменные в docker-compose.
Создаем docker-compose.yml
Хорошо бы вам про YAML формат почитать отдельно, а то статья и так уже растет, как на дрожжах. Для нас это просто еще одно описание переменных по типу .properties. Только в пропертях записывается через точку, а в YAML это делается немного красивее. Например, так. Две переменные в .properties: javarush.telegram.bot.name=ivan javarush.telegram.bot.token=pupkin А вот в .yaml (тоже самое что и .yml) будет это так:
javarush:
telegram:
bot:
name: ivan
token: pupkin
Второй вариант более красивый и понятный. Пробелы должны быть именно такие, как указаны выше. Как-нибудь переведем наши application.properties и application.yml. Для начала нужно его создать. В корне проекта создаем файл docker-compose.yml и записываем туда следующее:
version: '3.1'
services:
jrtb:
build:
context: .
environment:
- BOT_NAME=${BOT_NAME}
- BOT_TOKEN=${BOT_TOKEN}
restart: always
Первая строка — это версия docker-compose. services: говорит о том, что все следующие строки после этого (будут сдвинуты) — относятся к сервисам, которые мы настраиваем. У нас такой пока только один — java-приложение под названием jrtb. И уже под ним будут все его настройки. Например, build: context: . говорит о том, что мы будем искать Dockerfile в той же директории, что и docker-compose.yml. А вот секция environment: будет отвечать за то, чтобы мы передали в Dockerfile необходимые переменные среды (environment variables). Как раз то, что нам и нужно. Поэтому ниже мы переменные и передаем. Их docker-compose будет искать в переменных операционной среды сервера. Добавим их в баш скрипте.
Создаем баш скрипты
И последний шаг — создать баш скрипт. Создаем в корне проекта файл с именем start.sh и пишем туда следующее:
#!/bin/bash
# Pull new changes
git pull
# Prepare Jar
mvn clean
mvn package
# Ensure, that docker-compose stopped
docker-compose stop
# Add environment variables
export BOT_NAME=$1
export BOT_TOKEN=$2
# Start new deployment
docker-compose up --build -d
Первая строка нужна для всех баш скриптов: без нее работать не будет. А далее — просто набор команд в терминале, которые нужно выполнить. Я добавил комментарии в каждой команде, поэтому должно быть понятно. Единственное, что хочется объяснить — это то, что значит $1 и $2. Это две переменные, которые будут переданы в запуске баш скрипта. При помощи команды export они будут добавлены в переменные сервера и считаны уже в docker-compose. Это работает для убунты, для виндоуса, наверно, нет, но я не уверен. Теперь нужно добавить скрипт stop.sh, который будет останавливать работу. В нем будет несколько строк:
#!/bin/bash
# Ensure, that docker-compose stopped
docker-compose stop
# Ensure, that the old application won't be deployed again.
mvn clean
Здесь мы останавливаем docker-compose и зачищаем джарник проекта, который лежит еще с прошлой сборки. Делаем мы это для того, чтобы наш проект точно пересобирался. Были прецеденты, поэтому и добавляю) В итоге у на получается 4 новых файла:
- Dockerfile — файл для создания образа нашего приложения;
- docker-compose.yml — файл с настройкой того, как мы будем запускать наши контейнеры;
- start.sh — баш скрипт для развертывания нашего приложения;
- stop.sh — баш скрипт для остановки нашего приложения.
Также обновим версию нашего приложения с 0.2.0-SNAPSHOT на 0.3.0-SNAPSHOT. Добавим в RELEASE_NOTES описание к новой версии и немного отрефакторим то, что было:
# Release Notes ## 0.3.0-SNAPSHOT * JRTB-13: added deployment process to the project ## 0.2.0-SNAPSHOT * JRTB-3: implemented Command pattern for handling Telegram Bot commands ## 0.1.0-SNAPSHOT * JRTB-2: added stub telegram bot * JRTB-0: added SpringBoot skeleton project И в README добавим новый параграф с описанием того, как деплоить наше приложение:
## Deployment Deployment process as easy as possible: Required software: – terminal for running bash scripts – docker – docker-compose to deploy application, switch to needed branch and run bash script: $ bash start.sh ${bot_username} ${bot_token} That’s all. Разумеется, все пишет на английском. Уже как обычно, в нашей новосозданной ветке STEP_4_JRTB-13 создаем новый коммит с именем: JRTB-13: implement deployment process via docker и делаем пуш. Я перестаю подробно останавливаться на вещах, которые я уже описывал в прошлых статьях. Не вижу смысла повторять одно и тоже. К тому же, кто разобрался и сделал у себя, у того вопросов не возникнет. Это я о том, как создать новую ветку, как создать коммит, как запушить коммит в репозиторий.
Заключение
В этом руководстве мы попробовали самостоятельно работать с Docker. Для этого создали простой Spring проект с одной конечной точкой REST и собрали для него образ Docker. В результате научились запускать образ Docker внутри контейнера и протестировали конечную точку REST внутри этого образа.