You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

68 KiB

Задания

1. Установка Docker

Далее приведены инструкции с https://docs.docker.com/engine/install/debian/. Используйте виртуальную машину studX.myoffice.ru.

Настройте репозиторий Docker

$ sudo apt-get update
$ sudo apt-get install ca-certificates curl gnupg lsb-release
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg  
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Проверьте, что в файле /etc/apt/sources.list.d/docker.list строка репозитория правильная, соответствует вашему дистрибутиву (bullseye в примере ниже)

deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bullseye stable

Установите Docker

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Проверте, всё ли установлено корректно:

$ docker run hello-world

Hello from Docker.
This message shows that your installation appears to be working correctly.

2. Играем с BusyBox

Теперь, когда всё подготовлено, пора приняться за дело. В этом разделе нашей целью будет запуск контейнера Busybox и освоение команды docker run.

Для начала, запустите следующую команду:

$ docker pull busybox

Внимание. В зависимости от того, как вы установили Docker, вы можете увидеть сообщение permission denied (доступ запрещён) в ответ на вызов выше приведённой команды. Если вы на Mac, убедитесь, что Docker движок запущен. Если на Линукс, вам может потребоваться повысить права доступа с помощью команды sudo. В качестве альтернативного варианта вы можете добавить пользователя в Docker группу для решения этой проблемы.

Команда pull скачивает образ busybox из Docker реестра и сохраняет его в систему. Вы можете использовать команду docker images для вывода в консоль списка образов находящихся в вашей системе.

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              00f017a8c2a6        2 weeks ago         1.11 MB
hello-world         latest              48b5124b2768        2 months ago        1.84 kB

1.1 Запуск Docker

Великолепно! Теперь перейдём к запуску контейнера на основе этого образа. Для этого мы воспользуемся всемогущей командой docker run.

$ docker run busybox

Постойте, но ничего не произошло! Это баг? Ну, нет. Под капотом произошло много всего. Когда вы запустили команду run, клиент Docker нашёл образ (в нашем случае, busybox), загрузил контейнер и запустил команду внутри этого контейнера. Мы не указали никаких аргументов, так что контейнер загрузился, выполнил команду sh и процесс контейнера завершился. Ну, да, как-то обидно. Попробуем сделать что-нибудь поинтереснее.

$ docker run busybox echo "hello from busybox"
hello from busybox

Ура, наконец-то что-то вывелось. Теперь Docker запустил команду echo внутри контейнера, а затем вышел из него. Вы, наверное, заметили, что всё произошло очень быстро. А теперь представьте себе процесс загрузки виртуальной машины, выполнения в ней команды и её выключения. Ясно, почему говорят, что контейнеры быстрые! Чтобы узнать время выполнения попробуйте запустить последнюю команду со словом time в начале.

Давайте взглянем на команду docker ps. Она выводит на экран список всех запущенных контейнеров.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

В силу того, что ни один контейнер не запущен, выводится пустая строка. Попробуем более информативный вариант docker ps -a:

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS                          PORTS               NAMES
56fdebcf3df0        busybox             "echo hi"           About a minute ago   Exited (0) About a minute ago                       jovial_wozniak
5f585bdd9545        busybox             "echo hi"           About a minute ago   Exited (0) About a minute ago                       focused_golick
ad64717b0d60        busybox             "sh"                8 minutes ago        Exited (0) 8 minutes ago                            determined_hugle
c73ceb428f23        hello-world         "/hello"            25 minutes ago       Exited (0) 25 minutes ago                           sad_mestorf

То, что мы видим в выдаче — список всех контейнеров, которые были запущены ранее. Обратите внимание, что колонка STATUS показывает, что эти контейнеры остановились несколько минут назад.

Наверное вы думаете, существует ли способ запустить более одной команды в контейнере. Давайте попробуем:

$ docker run -it busybox sh
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # uptime
 05:45:21 up  5:58,  0 users,  load average: 0.00, 0.01, 0.04

Выполнение команды run с флагами -it подключает нас к интерактивному терминалу tty в контейнере. Теперь мы можем запустить столько команд, сколько захотим. Уделите немного времени запуску ваших любимых команд в этой консоли.

Опасная зона. Если вы любите рисковать, вы можете попробовать выполнить команду rm -rf bin. Убедитесь, что выполняете команду в контейнере, а не в основной операционной системе. Удалив данной командой папку bin не даст возможности запускать команды как ls, echo. После того, как всё перестанет работать, вы можете выйти из контейнера (выполним команду exit), а затем запустить контейнер заново docker run -it busybox sh. Так как Docker каждый раз создаёт новый контейнер, папка bin и команды должны быть опять доступны.

Создание слоя файловой системы и запуск контейнера могут быть разделены. Проделайте аналогичный выше пример, но с командами create, start. Запуск команды docker create -it busybox sh создаст слой файловой системы контейнера. Затем командой docker start -ia <id_контейнера> вы можете запустить выполнение контейнера в интерактивном режиме. Проанализируйте, что произошло при повторном запуске.

На этом тур по возможностям команды docker run завершён. Скорее всего, вы будете использовать эту команду довольно часто. Так что важно, чтобы мы поняли как с ней обращаться. Чтобы узнать больше о run, используйте docker run --help, и увидите полный список поддерживаемых флагов. Скоро мы увидим еще несколько способов использования docker run.

Давайте вкратце рассмотрим удаление контейнеров. Мы видели выше, что с помощью команды docker ps -a всё ещё можно увидеть остатки завершённых контейнеров. На протяжении этого занятия, вы будете запускать docker run несколько раз, и оставшиеся, покинутые контейнеры будут съедать дисковое пространство. Так что если они больше вам не понадобятся, вы можете взять за правило удалять контейнеры после завершения работы с ними. Для этого используется команда docker rm. Просто скопируйте ID (можно несколько) из вывода выше и передайте параметрами в команду.

$ docker rm 305297d7a235 ff0a5c3750b9
305297d7a235
ff0a5c3750b9

При удалении идентификаторы будут снова выведены на экран. Если нужно удалить много контейнеров, то вместо ручного копирования и вставки можно сделать так:

$ docker rm $(docker ps -a -q -f status=exited)

Эта команда удаляет все контейнеры, у которых статус exited. Флаг -q возвращает только численные ID, а флаг -f фильтрует вывод на основе предоставленных условий. Последняя полезная деталь — команде docker run можно передать флаг --rm, тогда контейнер будет автоматически удаляться при завершении. Это очень удобно для разовых запусков и экспериментов с Docker.

По образу и подобию можно удалять ненужные образы командой docker rmi.

1.2 Терминология

В предыдущем разделе мы использовали много специфичного для Docker жаргона, и многих это может запутать. Перед тем, как продолжать, давайте разберем некоторые термины, которые часто используются в экосистеме Docker.

Image (образ) - файловая система и параметры, с которыми будет произведён запуск. Образ не содержит изменяемого состояния и никогда не изменяется. В примере выше мы использовали команду docker pull чтобы скачать образ busybox.

Container (контейнер) - Создаётся на основе образа и запускает само приложение. Мы создали контейнер командой docker run, и использовали образ busybox, скачанный ранее. Список запущенных контейнеров можно увидеть с помощью команды docker ps.

Docker Daemon (Docker демон) - Фоновый сервис, запущенный в хост операционной системе, который отвечает за создание, запуск и уничтожение Docker контейнеров. Демон — это процесс, который запущен в операционной системе, с которой взаимодействует клиент.

Docker Client (Docker клиент) - Утилита командной строки, которая позволяет пользователю взаимодействовать с демоном. Существуют другие формы клиента, например, Kitematic, с графическим интерфейсом.

Docker Hub - Реестр Docker образов. Грубо говоря, архив всех доступных образов. Если нужно, то можно содержать собственный реестр и использовать его для получения образов.

2 Веб-приложения в Docker

Супер! Мы научились работать с docker run, поиграли с несколькими контейнерами и разобрались в терминологии. Вооруженные этими знаниями, мы готовы переходить к реальным задачам: разворачиванию веб-приложений с Docker!

2.1 Статичный сайт

Давайте начнем с малого. Вначале рассмотрим самый простой статический веб-сайт на nginx. Скачаем образ из Docker Hub, запустим контейнер и посмотрим, насколько легко будет запустить веб-сервер.

Поехали. Для одностраничного сайта нам понадобится заранее созданный образ и размещённый в реестре - nginx:latest. Можно запустить образ напрямую командой docker run.

$ docker run nginx:latest

Так как образа не существует локально, клиент сначала скачает образ из реестра, а потом запустит его. Если всё пройдёт без проблем, то вы увидите в терминале сообщения о запуске nginx. Теперь сервер запущен. Как увидеть сайт в действии? На каком порту работает сервер? И, что самое важное, как напрямую достучаться до контейнера из хост системы?

В нашем случае клиент не открывает никакие порты, так что нужно будет перезапустить команду docker run чтобы сделать порты публичными. Заодно давайте сделаем так, чтобы терминал не был прикреплен к запущенному контейнеру. В таком случае можно будет спокойно закрыть терминал, а контейнер продолжит работу. Этот режим называется detached.

$ docker run -d -P --name static-site nginx:latest
e61d12292d69556eabe2a44c16cbd54486b2527e2ce4f95438e504afb7b02810

Флаг -d открепит (--detach) терминал, флаг -P сделает все открытые порты публичными и случайными, и, наконец, флаг --name это имя, которое мы хотим дать контейнеру. Теперь можно увидеть порты с помощью команды docker port [CONTAINER].

$ docker port static-site
80/tcp -> 0.0.0.0:49153
80/tcp -> :::49153

Вы можете открыть http://localhost:49153 в своём браузере, создав предварительно туннель ssh -L 49153:localhost:49153 studX.myoffice.ru или протестировать сайт командой curl.

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

$ docker run -p 8888:80 --name static-site nginx:latest

Ключ -p устанавливает соответствие между портом хост операционной системы (8888) с портом контейнера (80).

Чтобы остановить контейнер запустите docker stop и укажите идентификатор (ID) контейнера.

Согласитесь, все было очень просто. Теперь, когда вы увидели, как запускать контейнеризованный веб-сервер, вам, наверное, интересно — а как создать свой Docker образ? Следующий раздел посвящён этой теме.

2.2 Docker образы

Мы касались образов ранее, но в этом разделе мы заглянем глубже: что такое Docker образы и как создавать собственные образы. Наконец, мы используем собственный образ чтобы запустить приложение и показать друзьям. Круто? Круто! Давайте начнем.

Образы это основы для контейнеров. В прошлом примере мы скачали (команда pull) образ под названием Busybox из регистра, и попросили клиент Docker запустить контейнер, основанный на этом образе. Чтобы увидеть список доступных локально образов, используйте команду docker images.

$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
busybox       latest    bc01a3326866   4 days ago      1.24MB
nginx         latest    76c69feac34e   5 days ago      142MB
debian        latest    43d28810c1b4   6 weeks ago     124MB
ubuntu        latest    2dc39ba059dc   8 weeks ago     77.8MB
hello-world   latest    feb5d9fea6a5   13 months ago   13.3kB

Это список образов, скачанных из реестра, а также тех, что сделаны самостоятельно (скоро увидим, как это делать). TAG — это конкретный снимок (snapshot) образа, а IMAGE ID — это соответствующий уникальный идентификатор образа.

Для простоты, можно относиться к образу как к git-репозиторию. Образы можно коммитить с изменениями, и можно иметь несколько версий. Если не указывать конкретную версию, то клиент по умолчанию использует latest. Например, можно скачать определенную версию образа ubuntu:

$ docker pull ubuntu:12.04

Чтобы получить новый Docker образ, можно скачать его из реестра (такого, как Docker Hub) или создать собственный. На Docker Hub есть десятки тысяч образов. Поиск среди них доступен как на сайте, так и из командной строки с помощью docker search.

Важно понимать разницу между базовыми и дочерними образами.

Base image (базовый образ) — это образ, который не имеет родительского образа. Обычно это образы с операционной системой, такие как ubuntu, busybox или debian.

Child image (дочерний образ) — это образ, построенный на базовых образах и обладающий дополнительной функциональностью.

Существуют официальные и пользовательские образы, и любые из них могут быть базовыми и дочерними.

Официальные образы — это образы, которые официально поддерживаются командой Docker. Обычно в их названии одно слово. В списке выше python, ubuntu, busybox и hello-world — базовые образы.

Пользовательские образы — образы, созданные простыми пользователями вроде нас. Они построены на базовых образах. Обычно, они называются по формату user/image-name.

2.3 Наш первый образ

Теперь, когда мы лучше понимаем, что такое образы и какие они бывают, самое время создать собственный образ. Цель этого раздела — создать образ с простым приложением на Flask. Для этого заданияя подготовлено маленькое приложение flask-app, которое выводит случайную картинку с кошкой. Склонируйте этот репозиторий к себе на локальную машину git clone <адрес-репозитория> и перейдите в папку с приложением.

Следующим шагом является создание образа с данным веб-приложением. Как говорилось выше, все пользовательские образы базируются на базовых образах. Так как приложение написано на Python, базовый образ следует выбрать с предустановленным Python 3. Точнее мы собираемся использовать python:3.8 версию python образа.

Теперь у нас есть все ингредиенты для создания собственных образов - работающее веб-приложение и базовый образ. Как мы будем подходить к этой задаче? Ответ - Dockerfile.

2.4 Dockerfile

Dockerfile — это простой текстовый файл, в котором содержится список команд Docker клиента. Это простой способ автоматизировать процесс создания образа. Самое классное, что команды в Dockerfile почти идентичны своим аналогам в Linux. Это значит, что в принципе не нужно изучать новый синтаксис, чтобы начать работать с докер-файлами.

В директории с приложением есть Dockerfile, но так как мы делаем все впервые, нам нужно создать его с нуля. Создайте новый пустой файл в любимом текстовом редакторе, и сохраните его в той же директории, где находится flask-приложение. Назовите файл Dockerfile.

Для начала укажем базовый образ. Для этого нужно использовать ключевое слово FROM.

FROM python:3.8

Следующим шагом обычно указывают команды для копирования файлов из текущей папки в файловую систему образа и установки зависимостей.

COPY . .
RUN pip install --no-cache-dir -r requirements.txt

Дальше нам нужно указать порт, который следует открыть. Наше приложение работает на порту 5000, поэтому укажем его:

EXPOSE 5000

Последний шаг — указать команду для запуска приложения. Это просто python ./app.py. Для этого используем команду CMD:

CMD ["python", "./app.py"]

Главное предназначение CMD — это сообщить контейнеру какие команды нужно выполнить при старте. Теперь наш Dockerfile готов. Вот как он выглядит:

FROM python:3.8

# set a directory for the app
WORKDIR /usr/src/app

# copy all the files to the container
COPY . .

# install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# tell the port number the container should expose
EXPOSE 5000

# run the command
CMD ["python", "./app.py"]

Теперь можно создать образ. Команда docker build занимается сложной задачей создания образа на основе Dockerfile.

Листинг ниже демонстрирует процесс. Перед тем, как запустите команду сами (не забудьте точку в конце), проверьте, чтобы там был ваш username. Username должен соответствовать тому, что использовался при регистрации на Docker hub. Если вы еще не регистрировались, то сделайте это до выполнения команды.

Команда docker build довольно проста: она принимает опциональный тег с флагом -t имя_пользователя/имя_образа и путь до директории, в которой лежит Dockerfile.

$ docker build -t имя_пользователя/имя_образа ./
Sending build context to Docker daemon 8.704 kB
Step 1 : FROM python:3-onbuild
# Executing 3 build triggers...
Step 1 : COPY requirements.txt /usr/src/app/
 ---> Using cache
Step 1 : RUN pip install --no-cache-dir -r requirements.txt
 ---> Using cache
Step 1 : COPY . /usr/src/app
 ---> 1d61f639ef9e
Removing intermediate container 4de6ddf5528c
Step 2 : EXPOSE 5000
 ---> Running in 12cfcf6d67ee
 ---> f423c2f179d1
Removing intermediate container 12cfcf6d67ee
Step 3 : CMD python ./app.py
 ---> Running in f01401a5ace9
 ---> 13e87ed1fbc2
Removing intermediate container f01401a5ace9
Successfully built 13e87ed1fbc2

Если у вас нет образа python:3.8, то клиент сначала скачает его, а потом возьмётся за создание вашего образа. Так что, вывод на экран может отличаться от приведённого выше. Если все прошло хорошо, то образ готов! Запустите docker images и увидите свой образ в списке.

Последний шаг — запустить образ и проверить его работоспособность:

$ docker run -p 80:5000 --name catsapp имя_пользователя/имя_образа
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.  
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.3:5000
Press CTRL+C to quit

Зайдите на http://studX.myoffice.ru и увидите приложение в работе.

Поздравляем! Вы успешно создали свой первый Docker образ!

3 Многоконтейнерные окружения

В прошлом заданиях мы увидели, как легко и просто запускать приложения с помощью Docker. Мы начали с простого статического сайта, а потом запустили Flask-приложение. Оба варианта можно было запускать локально или в облаке, несколькими командами. И то, и другое приложения работали в одном контейнере.

Современные приложения не такие простые. Как правило всегда используется база данных или другой тип постоянного хранилища. Системы Redis и Memcached стали практически обязательной частью архитектуры веб-приложения. Поэтому, в этом разделе мы научимся контейнеризировать приложения, которым требуется несколько запущенных сервисов.

В частности, мы увидим, как запускать и управлять многоконтейнерными Docker-окружениями. Почему нужно несколько контейнеров, спросите вы? Ну, одна из главных идей Docker в том, что он предоставляет изоляцию. Идея совмещения процесса и его зависимостей в одной песочнице (называемой контейнером) и делает Docker мощным инструментом.

Когда монолитное приложение становится слишком сложным для поддержки в распределённой среде мудрым решением является произвести его декомпозицию на компоненты-сервисы. Хорошей идеей является содержание сервисов в отдельных контейнерах. Разным компонентам скорее всего потребуются разные ресурсы, и необходимость в новых ресурсах может возникать в разной степени. Помещая компоненты в отдельные контейнеры, мы можем выделять наиболее подходящий тип ресурсов для каждой части приложения. Это также созвучно со всем микросервисным движением. Это одна из причин, по которой Docker (и любая другая технология контейнеризации) находится на передовой современных микросервисных архитектур.

3.1 Приложение для поиска фургонов c едой в Сан-Франциско

Приложение, которое мы переведём в Docker, называется SF Food Trucks. Приложение создавалось с целью сделать что-то похожее на реально эксплуатируемое приложение и приносещее пользу, но не слишком сложное.

Серверная часть написана на Python (Flask framework), а для поиска используется Elasticsearch. Как и всё остальное в этом обучении, код проекта находится на Github. Мы используем это приложение, чтобы научиться запускать и разворачивать многоконтейнерное окружение.

Код проекта вы можете найти в папке с заданием.

Теперь, когда вы воодушевлены (будем надеятся), давайте подумаем, как будет выглядеть этот процесс. В нашем приложении есть бэкенд на Flask и сервис Elasticsearch. Естественным образом можно разделить приложение на два контейнера: один для Flask, другой для Elasticsearch (ES). Если приложение станет популярным, мы сможем масштабировать приложение добавлением новых контейнеров для тех компонентов, которые станут узким местом.

Отлично, значит нам нужно два контейнера. Это не сложно, правда? Мы уже создавали Flask-контейнер в прошлый раз. А для Elasticsearch... давайте посмотрим, есть ли что-нибудь в репозитории. Зайдём на сайт реестра Docker https://hub.docker.com/ и наберём имя в поиске.

Не удивительно, что существует официальный образ для Elasticsearch. Чтобы запустит ES, нужно всего лишь выполнить docker run, и вскоре у нас будет локальный, работающий контейнер с одним узлом ES. Некоторые компании считают плохой практикой выкладывать образы с тегом latest, в том числе Elastic, поэтому выберем и укажеи тег явно.

Замечание. Если контейнер не запускается, попробуйте подключиться в интерактивном режиме (с ключом -it) и выяснить причину ошибки. Одной из причин может быть нехватка памяти для Elasticsearch. Для версии старше 5 требуется минимум 2 Гб. Проблема также может быть в хост машине. Увеличьте количество доступных кусочков памяти процессу выполнением команды sysctl -w vm.max_map_count=262144. Обратите внимание, что это модифицирует свойство системы, в которой может выполняться множество других контейнеров.

$ docker run -d -p 9200:9200 --name elastic elasticsearch:8.4.3
d582e031a005f41eea704cdc6b21e62e7a8a42021297ce7ce123b945ae3d3763

При запуске в режиме демона сервис не генерирует автоматически пароль для пользователя elastic. Мы можем сделать это сами после запуска, чтобы протестировать сервис.

docker exec -ti elastic /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
WARNING: Owner of file [/usr/share/elasticsearch/config/users] used to be [root], but now is [elasticsearch]
WARNING: Owner of file [/usr/share/elasticsearch/config/users_roles] used to be [root], but now is [elasticsearch]
This tool will reset the password of the [elastic] user to an autogenerated value.
The password will be printed in the console.
Please confirm that you would like to continue [y/N]y


Password for the [elastic] user successfully reset.
New value: 36EBVQtPjbiFPMh9Bk7X
$ curl https://localhost:9200 --insecure --user elastic:36EBVQtPjbiFPMh9Bk7X
{
  "name" : "1d5f2c03f376",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "GjsgucoGTyOG5m8tWgItpA",
  "version" : {
    "number" : "8.4.3",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
    "build_date" : "2022-10-04T07:17:24.662462378Z",
    "build_snapshot" : false,
    "lucene_version" : "9.3.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}

Заодно давайте запустим контейнер с Flask. Но вначале нужен Dockerfile. В прошлой секции мы использовали образ python:3.8 в качестве базового. Однако, в этом раз, кроме установки зависимостей через pip, нам нужно, чтобы приложение генерировало Javascript файл. Для этого потребуется Nodejs. В связи с появлением дополнительных файлов для работы контейнера нам нужно построить новый образ. Начнем с базового образа debian:buster.

Замечание. Если оказывается, что существующий образ не подходит для вашей задачи, то спокойно создавайте свой образ на основе другого базового образа. В большинстве случаев, для образов на Docker Hub можно найти соответствующий Dockerfile на Github. Почитайте существующие докерфайлы — это один из лучших способов научиться делать свои образы.

Наш Dockerfile для Flask-приложения выглядит следующим образом:

# start from base
FROM ubuntu:focal

# install system-wide deps for python and node
RUN apt update
RUN apt -y install python3 python3-pip curl
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash
RUN apt-get install -yq nodejs

# copy our application code
ADD flask-app /opt/flask-app
WORKDIR /opt/flask-app

# fetch app specific deps
RUN npm install
RUN npm run build
RUN pip3 install -r requirements.txt

# expose port
EXPOSE 5000

# start app
ENTRYPOINT [ "python3", "./app.py" ]

Тут много всего нового. Вначале указан focal образ Ubuntu, потом используется пакетный менеджер apt для установки зависимостей, в частности: Python и Node. Флаг y нужен автоматического выбора "Yes" во всех диалогах. Также создается символическая ссылка для бинарного файла node. Это нужно для решения проблем обратной совместимости.

Потом мы используем команду ADD для копирования приложения в нужную директорию в контейнере — /opt/flask-app. Здесь будет находиться весь наш код. Мы также устанавливаем эту директорию в качестве рабочей, так что следующие команды будут выполняться в контексте этой локации. Теперь, когда наши системные зависимости установлены, пора установить зависимости уровня приложения. Начнем с Node, установки пакетов из npm и запуска команды сборки, как указано в нашем package.json файле. В конце устанавливаем пакеты Python, открываем порт и определяем запуск приложения с помощь ENTRYPOINT. ENTRYPOINT отличается от CMD тем, что при запуске можно передвать в контейнер параметры, которые добавятся к ENTRYPOINT.

Наконец, можно собрать образ и запустить контейнер.

$ docker build -t studX/<имя образа>.

При первом запуске нужно будет больше времени, так как клиент Докера будет скачивать образ ubuntu, запускать все команды и готовить образ. Повторный запуск docker build после последующих изменений будет практически моментальным. Давайте попробуем запустить приложение.

$ docker run -P studX/<имя образа>
Unable to connect to ES. Retying in 5 secs...
Unable to connect to ES. Retying in 5 secs...
Unable to connect to ES. Retying in 5 secs...
Out of retries. Bailing out...

Упс! Наше приложение не смогло запуститься, потому что оно не может подключиться к Elasticsearch. Как сообщить одному контейнеру о другом и как заставить их взаимодействовать друг с другом? Ответ в следующей секции.

3.2 Сетевая инфраструктура Docker

Перед тем, как обсудить возможности Docker для решения описанной задачи, давайте посмотрим на возможные варианты обхода проблемы. Думаю, это поможет нам оценить удобство той функциональности, которую мы вскоре изучим.

Ладно, давайте запустим docker ps. Что тут у нас:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                              NAMES
c31bf3beb299        elasticsearch       "/docker-entrypoin..."   2 hours ago         Up 2 hours          0.0.0.0:9200->9200/tcp, 9300/tcp   tender_wilson

Итак, у нас есть контейнер ES по любому локальному адресу (0.0.0.0) и порту 9200, и мы можем напрямую обращаться к нему. Если можно было бы сообщить нашему приложению подключаться к этому адресу, то оно сможет общаться с ES, верно? Давайте взглянем на код на Питоне, туда, где описано подключение.

es = Elasticsearch(
        f"{es_scheme}://{es_host}:{es_port}", 
        basic_auth=(es_user, es_password),
        verify_certs=False
)

Для того, чтобы это заработало, нужно запустить Flask контейнер на том же хосте, что и контейнер ES, и всё заработает, да? К сожалению, нет, потому что контейнер ES доступен по адресу хост-машины только с хост-машины. Другой контейнер не сможет обратиться по этому адресу. Ладно, если не этот адрес, то какой тогда адрес нужно использовать для работы с контейнером ES? Рады, что вы спросили.

Подошло время, чтобы изучить работу сети в Docker. После установки, Docker автоматически создает три сети:

$ docker network ls
NETWORK ID          NAME                DRIVER
075b9f628ccc        none                null
be0f7178486c        host                host
8022115322ec        bridge              bridge

Сеть bridge — это сеть, в которой контейнеры запущены по умолчанию (программный роутер). Это значит, что когда вы запускаете контейнер ES, он работает в bridge сети. Чтобы удостовериться, давайте проверим:

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "5c06e23b03c1d834ee7b3ac998c68c7020c959af3822db7c2629e9cd842f3de3",
        "Created": "2022-10-24T21:47:51.003825084+04:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "1ea1508b85bc9cf7edc3a3401fdcd69f82ed86e9d88f5d52732eb2040ba64896": {
                "Name": "relaxed_gates",
                "EndpointID": "14ce3acff72602eac537644f9ea7d2e2c7b1986156a3cf05313830d880eb50e8",  
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "a739a33489d98a4857c0d9bceefd03d930df5015c8f3ac04180d2d8b81fb050d": {
                "Name": "elastic",
                "EndpointID": "1db7a2319e6e890c1d547b6a8eb717a0628dfa4276d339686c0b6dc9f3dcb677",  
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

Видно, что контейнер e931ab24dedc находится в секции Containers. Также виден IP-адрес, выданный этому контейнеру — 172.17.0.2. Именно этот адрес мы и искали? Давайте проверим: запустим Flask-приложение и попробуем обратиться к нему по IP:

$ docker run -it --rm studX/<имя образа> --name flaskapp bash
root@35180ccc206a:/opt/flask-app# curl https://172.17.0.2:9200 --insecure --user elastic:36EBVQtPjbiFPMh9Bk7X
{
  "name" : "a739a33489d9",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "WFQj_5KiSW2shK9eLDCyOQ",
  "version" : {
    "number" : "8.4.3",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
    "build_date" : "2022-10-04T07:17:24.662462378Z",
    "build_snapshot" : false,
    "lucene_version" : "9.3.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}
root@35180ccc206a:/opt/flask-app# exit

Cейчас всё должно стать понятно. Мы запустили контейнер в интерактивном режиме с процессом bash. Флаг --rm нужен для удобства, благодаря нему контейнер автоматически удаляется после выхода. Мы попробуем curl, но нужно сначала установить его. После этого можно удостовериться, что по адресу 172.17.0.2:9200 на самом деле можно обращаться к ES! Супер!

Не смотря на то, что мы нашли способ наладить связь между контейнерами, существует несколько проблем с этим подходом:

Придется добавлять записи в файл /etc/hosts (локальный DNS) внутри Flask-контейнера, чтобы приложение понимало, что имя хоста es означает 172.17.0.2. Если IP-адрес меняется, то придется вручную менять запись.

Так как сеть bridge используется всеми контейнерами по умолчанию, этот метод не безопасен.

Но есть хорошие новости: в Docker есть отличное решение этой проблемы. Docker позволяет создавать собственные изолированные сети. Это решение также помогает справиться с проблемой /etc/hosts, сейчас увидим как.

Во-первых, давайте создадим свою сеть.

$ docker network create foodtrucks-network
1a3386375797001999732cb4c4e97b88172d983b08cd0addfcb161eed0c18d89
$ docker network ls
NETWORK ID          NAME                DRIVER
1a3386375797        foodtrucks-network  bridge
8022115322ec        bridge              bridge
075b9f628ccc        none                null
be0f7178486c        host                host

Команда network create создаёт новую сеть bridge. Нами сейчас нужен именно этот тип сети. Существуют и другие типы, о которых вы можете прочитать в официальной документации.

Теперь у нас есть сеть. Можно запустить наши контейнеры внутри сети с помощью флага --net. Давайте так и сделаем, но сначала остановим контейнер с ElasticSearch, который был запущен в сети bridge по умолчанию.

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                              NAMES
e931ab24dedc        elasticsearch       "/docker-entrypoint.s"   4 hours ago         Up 4 hours          0.0.0.0:9200->9200/tcp, 9300/tcp   cocky_spence
$ docker stop e931ab24dedc
e931ab24dedc
$ docker run -d -p 9200:9200 --net foodtrucks-network --name elastic elasticsearch:8.4.3
2c0b96f9b8030f038e40abea44c2d17b0a8edda1354a08166c33e6d351d0c651
$ docker network inspect foodtrucks-network
[
    {
        "Name": "foodtrucks-network",
        "Id": "de0befdc7aac3df9561249b3d1315c72f2b4a8de1073f95cccb576f74757cd1b",
        "Created": "2022-10-31T13:05:00.846504587+04:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "fe09955428cce79b76a590074049c8ade89fd0ead603082a4ca667320dac8c9a": {
                "Name": "elastic",
                "EndpointID": "e142fcf3ff129db8a6aed7fa73399378d3a05c20a0697930324f30ff8506dea0",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Мы сделали то же, что и раньше, но на этот раз присоединили контейнер к сети foodtrucks. Перед тем, как запускать контейнер с приложением, давайте проверим что происходит, когда запуск происходит в сети.

$ docker run -it --rm --net foodtrucks-network studX/<имя образа> bash
root@53af252b771a:/opt/flask-app# curl https://elastic:9200 --insecure --user elastic:v7SLsbtXticPLADei5vS
{
  "name" : "fe09955428cc",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "-2SKtN0ISFCJ0jm6Z7TrZQ",
  "version" : {
    "number" : "8.4.3",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
    "build_date" : "2022-10-04T07:17:24.662462378Z",
    "build_snapshot" : false,
    "lucene_version" : "9.3.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}
root@53af252b771a:/opt/flask-app# ls
app.py  node_modules  package.json  requirements.txt  static  templates  webpack.config.js
root@53af252b771a:/opt/flask-app# python3 app.py https elastic 9200 elastic v7SLsbtXticPLADei5vS
Index not found...
Loading data in elasticsearch ...
Total trucks loaded:  733
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
root@53af252b771a:/opt/flask-app# exit

Ура! Работает! Магическим образом Docker теперь разрешает имя es в нужный ip, и поэтому es:9200 можно использовать в приложении — этот адрес корректно направляет запросы в контейнер ES. Отлично! Давайте теперь запустим Flask-контейнер по-настоящему:

$ docker run -d --net foodtrucks -p 80:5000 --name foodtrucks-web <имя пользователя Docker>/<имя образа>
2a1b77e066e646686f669bab4759ec1611db359362a031667cacbe45c3ddb413
$ docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                              NAMES
2a1b77e066e6        prakhar1989/foodtrucks-web   "python ./app.py"        2 seconds ago       Up 1 seconds        0.0.0.0:5000->5000/tcp             foodtrucks-web
2c0b96f9b803        elasticsearch                "/docker-entrypoint.s"   21 minutes ago      Up 21 minutes       0.0.0.0:9200->9200/tcp, 9300/tcp   es

Зайдите на http://studX.myoffice.ru, и увидите приложение в работе. Опять же, может показаться, что было много работы, но на самом деле мы ввели всего 4 команды чтобы с нуля дойти до работающего приложения. Все команды, которые мы проделали собраны в bash скрипте.

#!/bin/bash
# build the flask container
docker build -t studX/foodtrucks-web ./

# create the network
docker network create foodtrucks-network

# start the ES container
docker run -d --net foodtrucks-network --name elastic elasticsearch:8.4.3

# start the flask app container 
# point to elastic host: https://elastic:9200 user: elastic password: v7SLsbtXticPLADei5vS
docker run -d -p 80:5000 --net foodtrucks-network studX/foodtrucks-web https elastic 9200 elastic v7SLsbtXticPLADei5vS

Теперь представьте, что хотите поделиться приложением с другом. Или хотите запустить его на сервере, где установлен Docker. Можно запустить всю систему с помощью одной команды!

$ git clone https://github.com/studX.myoffice/foodtrucks-app
$ cd foodtrucks-app
$ ./build_and_run.sh

Вот и всё! По-моему, это невероятно крутой и мощный способ распространять и запускать приложения!

3.3 Docker Compose

До этого момента мы изучали клиент Docker. Но в Docker экосистеме есть несколько других инструментов с открытым исходным кодом, которые хорошо взаимодействуют с Docker. Некоторые из них это:

Docker Compose — инструмент для определения и запуска многоконтейнерных приложений.

Docker Swarm — решение для кластерных приложений.

В этом разделе мы поговорим об одном из этих инструментов — Docker Compose, и узнаем, как он может упростить работу с несколькими контейнерами.

Итак, зачем используется Compose? Это инструмент для простого определения и запуска многоконтейнерных Docker приложений. В нем есть файл docker-compose.yml, и с его помощью можно одной командой поднять приложение с набором сервисов. docker-compose.yml - это замена скрипту, без этапа построения образа.

Давайте посмотрим, сможем ли мы создать файл docker-compose.yml для нашего приложения и проверим, способен ли он на то, что обещает.

Однако вначале нужно установить Docker Compose. Есть у вас Windows или Mac, то Docker Compose уже установлен — он идет в комплекте с Docker Toolbox. На Linux можно установить Docker Compose, следуя простым инструкциям на сайте документации https://docs.docker.com/compose/install/other/.

curl -SL https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

Проверить работоспособность так:

$ docker-compose version
Docker Compose version v2.12.2

Теперь можно перейти к следующему шагу, то есть к созданию файла docker-compose.yml. Синтаксис yml-файлов очень простой (но отступы коварны):

version: "3"
services:
  elastic:
    image: elasticsearch:8.4.3
    environment:
      - ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS
  foodtrucks-web:
    image: stud15/foodtrucks-web
    command: https elastic 9200 elastic v7SLsbtXticPLADei5vS
    depends_on:
      - elastic
    ports:
      - 80:5000

Давайте разберём это подробнее. На родительском уровне мы задали название неймспейса для наших сервисов: elastic и foodtrucks-web. К каждому сервису можно добавить дополнительные параметры, среди которых image — обязательный. Для elastic мы указываем доступный на Docker Hub образ elasticsearch. Для Flask-приложения — тот образ, который мы создали самостоятельно до этого.

С помощью других параметров вроде command и ports можно предоставить информацию о контейнере. Подробнее о параметрах и возможных значениях можно прочитать в документации.

Замечание. Нужно находиться в директории с файлом docker-compose.yml чтобы запускать большую часть команд Compose.

Отлично! Файл готов, давайте посмотрим на docker-compose в действии. Но вначале нужно удостовериться, что порты свободны. Так что если у вас запущены контейнеры Flask и ES, то пора их остановить:

$ docker stop $(docker ps -q)
39a2f5df14ef
2a1b77e066e6

Теперь можно запускать docker-compose. Перейдите в директорию с приложением Foodtrucks и выполните команду docker-compose up.

$ docker-compose up
[+] Running 2/0
 ⠿ Container elastic         Created                                                                               0.0s 
 ⠿ Container foodtrucks-web  Created                                                                               0.0s 
Attaching to elastic, foodtrucks-web
elastic         | {"@timestamp":"2022-10-31T12:01:38.948Z", "log.level": "INFO", "message":"version[8.4.3], pid[118], build[docker/42f05b9372a9a4a470db3b52817899b99a76ee73/2022-10-04T07:17:24.662462378Z], OS[Linux/5.10.0-16-amd64/amd64], JVM[Oracle Corporation/OpenJDK 64-Bit Server VM/18.0.2.1/18.0.2.1+1-1]", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.node.Node","elasticsearch.node.name":"5f10b96ad01d","elasticsearch.cluster.name":"docker-cluster"}
...
foodtrucks-web  |   warnings.warn(
foodtrucks-web  | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
foodtrucks-web  |  * Running on all addresses (0.0.0.0)
 server instead.
foodtrucks-web  |  * Running on all addresses (0.0.0.0)
foodtrucks-web  |  * Running on http://127.0.0.1:5000
foodtrucks-web  |  * Running on http://172.19.0.3:5000
foodtrucks-web  | Press CTRL+C to quit
foodtrucks-web  | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET / HTTP/1.1" 200 -
foodtrucks-web  | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET /static/styles/main.css HTTP/1.1" 200 -
foodtrucks-web  | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET /static/build/main.js HTTP/1.1" 200 -
foodtrucks-web  | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET /static/build/main.js.map HTTP/1.1" 200 -
foodtrucks-web  | 172.19.0.1 - - [31/Oct/2022 11:56:16] "GET /static/build/main.js.map HTTP/1.1" 304 -
^CGracefully stopping... (press Ctrl+C again to force)

Перейдите по IP чтобы увидеть приложение. Круто, да? Всего лишь пара строк конфигурации и несколько Докер-контейнеров работают в унисон. Давайте остановим сервисы и перезапустим в detached mode:

[+] Running 2/2
 ⠿ Container foodtrucks-web  Stopped                                                                              10.9s 
 ⠿ Container elastic         Stopped  
# docker-compose up -d
[+] Running 2/2
 ⠿ Container elastic         Started                                                                               0.7s 
 ⠿ Container foodtrucks-web  Started                                                                               1.4s
$ docker-compose ps
elastic             "/bin/tini -- /usr/l…"   elastic             running             0.0.0.0:9200->9200/tcp, :::9200->9200/tcp, 9300/tcp
foodtrucks-web      "python3 ./app.py ht…"   foodtrucks-web      running             0.0.0.0:8000->5000/tcp, :::8000->5000/tcp

Проверим, создались ли какие-нибудь сети:

$ docker network ls
NETWORK ID     NAME                 DRIVER    SCOPE
5c06e23b03c1   bridge               bridge    local
de0befdc7aac   foodtrucks           bridge    local
429ee73da4ea   foodtrucks_default   bridge    local
e9a9ee381df4   host                 host      local
7e9bc47cad68   none                 null      local

Видно, что Compose самостоятельно создал сеть foodtrucks_default и подсоединил оба сервиса в эту сеть, так, чтобы они могли общаться друг с другом. Каждый контейнер для сервиса подключен к сети, и оба контейнера доступны другим контейнерам в сети. Они доступны по hostname, который совпадает с названием контейнера. Давайте проверим, находится ли эта информация в /etc/hosts.

$ docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                    NAMES
bb72dcebd379        prakhar1989/foodtrucks-web   "python app.py"          20 hours ago        Up 19 hours         0.0.0.0:5000->5000/tcp   foodtrucks_web_1
3338fc79be4b        elasticsearch                "/docker-entrypoint.s"   20 hours ago        Up 19 hours         9200/tcp, 9300/tcp       foodtrucks_es_1
$ docker exec -it bb72dcebd379 bash
root@bb72dcebd379:/opt/flask-app# cat /etc/hosts
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.18.0.2	bb72dcebd379

Упс! Оказывается, файл понятия не имеет о elastic. Как же наше приложение работает? Давайте попингуем его по названию хоста:

root@bb72dcebd379:/opt/flask-app# ping elastic
PING es (172.18.0.2) 56(84) bytes of data.
64 bytes from foodtrucks_es_1.foodtrucks_default (172.18.0.2): icmp_seq=1 ttl=64 time=0.049 ms
64 bytes from foodtrucks_es_1.foodtrucks_default (172.18.0.2): icmp_seq=2 ttl=64 time=0.064 ms
^C
--- es ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.049/0.056/0.064/0.010 ms

Вуаля! Работает! Каким-то магическим образом контейнер смог сделать пинг хоста elastic. Оказывается, Docker 1.10 добавили новую сетевую систему, которая производит обнаружение сервисов через DNS-сервер. Если интересно, то почитайте подробнее о предложении в release notes.

На этом наш тур по Docker Compose завершен. С этим инструментом можно ставить сервисы на паузу, запускать отдельные команды в контейнере и даже масштабировать систему, то есть увеличивать количество контейнеров.

Надеюсь, проделанные нами действия продемонстрировали как на самом деле просто управлять многоконтейнерной средой с Compose.

4 Заключение

Мы подошли к концу. После длинного, изматывающего, но интересного пособия вы готовы захватить мир контейнеров! Если вы следовали пособию до самого конца, то можете заслуженно гордиться собой. Вы научились устанавливать Docker, запускать свои контейнеры, запускать статические и динамические веб-сайты.

Релевантные источники