Added docker_basic basic command tasks.

Added .editorconfig

Added directory structure to docker-related and monitoring-related sections.
pull/1/head
Dmitry Ignatiev 1 year ago
parent 74617ad9aa
commit dd2461a163

@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.{yml,xml,json,js,ts,md}]
indent_style = space
indent_size = 2
[*.{py}]
indent_style = space
indent_size = 4

@ -69,16 +69,7 @@ Docker Hub - это общедоступная служба реестра, по
В вашей компании вы можете поднять собственный реестр на основе одного из общедоступных образов реестров. При выборе образа реестра для производственной среды нужно учесть ряд требований к хранилищу данных, к аутентификации и авторизации, и к требованиям выдвигаемым другими задача обслуживания.
## Инструменты Docker
### 1. Многоконтейнерные приложения
Docker Compose — это инструмент для запуска многоконтейнерных приложений в Docker, определенных с использованием формата файлов Compose. Файл Compose используется для определения того, как настроены один или несколько контейнеров, составляющих ваше приложение. Когда у вас есть файл Compose, вы можете создать и запустить приложение с помощью одной команды: docker compose up. В качестве типичного примера приложения можно привести веб-сайт, состоящий из контейнера фронтэнда и контейнера бэкенда.
### 2. Кластерные приложения
Docker Swarm это простой оркестратор для контейнеров, который доступен из коробки. Позволяет объединить докер демоны на разных машинах в кластер, что даёт возможность поставки распределённых приложений упакованных в контейнеры. Swarm, так же как и Compose, имеет декларативную модель описания кластерного приложения. Типичными задачами для такого типа приложений являются организация высокой доступности, балансировки нагрузки и канареечного обновления сервиса. Также вы можете добиться эластичности распределённого приложения, которое создаёт и удаляет контейнеры в зависимости от поступающей нагрузки.
### 5. Volumes
## Релевантные источники

@ -6,7 +6,6 @@
4. Как запустить контейнер в режиме демона?
5. Как вывести список работающих контейнеров?
6. Как посмотреть логи контейнера?
7. Для чего используется инструмент docker compose?
8. Какие механизмы ядра используются Docker?
9. Какой демон процесс отвечает за работу контейнеров и общение с реестром Docker?
10. Какой конфигурационный файл используется для построения образа?

@ -0,0 +1,179 @@
# Решения
### 1. Установка Docker
Инструкции доступны на https://docs.docker.com/engine/install/debian/
#### Добавление GPG ключа официального репозитория Docker
````bash
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
````
#### Добавление репозитория в apt
````bash
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
````
#### Установка пакетов Docker
````bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
````
### 2. Базовые команды
#### 1.
````bash
sudo docker run hello-world
````
#### 2.
````bash
sudo docker pull busybox
````
#### 3.
````bash
sudo docker run busybox echo 'Hello, World!'
````
#### 4.
````bash
sudo docker run -it busybox
````
Внутри контейнера:
````bash
echo 'Hello, World!'
exit
````
Вообще, под "интерактивным режимом" здесь понимается именно два флага: `-i` и `-t`.\
Флаг `-i`, или собственно `--interactive`, означает, что stdin контейнера открыт и идет с хоста.\
А вот флаг `-t`, хотя и не обязателен, создает для контейнера псевдо-TTY. Чем чревато его отсутствие -\
студентам надо погуглить или напороться на это самим.
#### 5.
````bash
sudo docker run --name hello_world busybox echo 'Hello, World!'
````
После этого:
````bash
sudo docker start -a hello_world
````
Здесь важен флажок `-a`, т.е. чтобы stdout контейнера приаттачился, чтобы 'Hello, World!' вывелся на экран.
#### 6.
````bash
sudo docker create --name hello_world_delayed_start busybox echo 'Hello, World!'
sudo docker start -a hello_world_delayed_start
````
#### 7.
````bash
sudo docker run -d --name infinite_print busybox ash -c 'while true; do date +%T; sleep 1; done'
````
Для просмотра логов:
````bash
sudo docker logs infinite_print
````
#### 8.
Для вывода работающих контейнеров:
````bash
sudo docker ps
````
или
````bash
sudo docker container ls
````
Для вывода всех контейнеров:
````bash
sudo docker ps -a
````
или
````bash
sudo docker container ls -a
````
Для вывода остановленных контейнеров:
````bash
sudo docker ps -a -f status=exited -f status=created
````
или
````bash
sudo docker container ls -a -f status=exited -f status=created
````
#### 9.
````bash
sudo docker container prune -f
````
или же
````bash
sudo docker rm $(docker ps -a -q -f status=exited -f status=created)
````
#### 10.
````bash
sudo docker attach infinite_print
````
В параллельном терминале, для паузы:
````bash
sudo docker pause infinite_print
````
Для возобновления работы:
````bash
sudo docker unpause infinite_print
````
Для детача надо нажать комбинацию C-p C-q, но тут есть хитрость:
Для того, чтобы это было возможно, надо чтобы контейнер, запущенный с -d был запущен еще с -it
Т.е.
````bash
sudo docker run -dit --name infinite_print busybox ash -c 'while true; do date +%T; sleep 1; done'
````
Для остановки(мгновенно):
````bash
sudo docker kill infinite_print
````
или (если главный процесс не отвечает, придется немного подождать, прежде чем ему придет SIGKILL)
````bash
sudo docker stop infinite_print
````
или (но тогда контейнер будет не только остановлен но и удален)
````bash
sudo docker rm -f infinite_print
````
#### 11.
````bash
echo 'Hello, World!' > ~/hello.txt
sudo docker run -it --name hello_with_file busybox
````
В параллельном терминале:
````bash
sudo docker cp ~/hello.txt hello_with_file:/
````
В терминале с интерактивным контейнером:
````bash
cat /hello.txt
````
#### 12.
Листинг корневой директории:
````bash
sudo docker exec hello_with_file ls /
````
Дифф файловой системы:
````bash
sudo docker diff hello_with_file
````
Удаление файла:
````bash
sudo docker exec hello_with_file rm /hello.txt
````

@ -1,905 +1,53 @@
# Задания
## 1. Установка Docker
### 1. Установка
Далее приведены инструкции с https://docs.docker.com/engine/install/debian/. Используйте виртуальную машину `studX.myoffice.ru`.
Установите Docker на виртуальную машину
Настройте репозиторий 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
```
### 2. Базовые команды
Проверьте, что в файле `/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
```
1. Запустите Docker hello-world
Установите Docker
```
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
```
2. Загрузите образ busybox с Docker Hub
Проверьте, всё ли установлено корректно:
```
$ sudo docker run hello-world
3. Напечатайте 'Hello from busybox!', запустив контейнер на основе образа busybox с командой echo
Hello from Docker.
This message shows that your installation appears to be working correctly.
```
4. Запустите контейнер с busybox в интерактивном режиме и напечатайте 'Hello, World!' изнутри. После этого завершите выполнение контейнера.
## 2. Играем с BusyBox
5. Запустите контейнер как в пункте 3, но присвойте ему имя.\
После того, как его выполнение завершится, запустите его же - еще раз, по имени. Убедитесь что он выведет то же самое.
Теперь, когда всё подготовлено, пора приняться за дело. В этом разделе нашей целью будет запуск контейнера Busybox и освоение команды docker run.
6. Создайте именованный контейнер как в пункте 3, но так, чтобы он не запустился сразу же. Запустите его руками.
Для начала, запустите следующую команду:
```
$ sudo docker pull busybox
```
7. Запустите контейнер с busybox, который каждую секунду печатает текущее время, но запустите его в отвязанном от терминала режиме.\
Так чтобы он не печатал ничего вам на стандартный вывод. \
Посмотрите логи этого контейнера, и убедитесь что он работает.
**Внимание.** В зависимости от того, как вы установили Docker, вы можете увидеть сообщение permission denied (доступ запрещён) в ответ на вызов выше приведённой команды. Если вы на Mac, убедитесь, что Docker движок запущен. Если на Линукс, вам может потребоваться повысить права доступа с помощью команды `sudo`. В качестве альтернативного варианта вы можете добавить пользователя в Docker группу для решения этой проблемы.
8. Выведите список всех контейнеров. Сначала работающих, потом - всех, даже остановленных. Потом - только остановленных.
Команда `pull` скачивает образ `busybox` из Docker реестра и сохраняет его в систему. Вы можете использовать команду `docker images` для вывода в консоль списка образов находящихся в вашей системе.
```
$ sudo 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
```
9. Удалите все остановленные контейнеры, включая те, что так и не запустились, если такие были.
10. Приаттачьтесь к терминалу запущенного контейнера, который печатает текущее время каждую секунду, так чтобы увидеть поток этих строк.\
В параллельном терминале, поставьте выполнение контейнера на паузу, но без его полной остановки.\
Убедитесь что печать приостановилась.\
Возобновите работу контейнера.\
Сделайте детач терминала от контейнера, но так, чтобы он не остановился, а продолжил работу.\
В конечном итоге остановите контейнер извне.
### 1.1 Запуск Docker
11. Создайте какой-либо текстовый файл.\
Запустите контейнер с busybox в интерактивном режиме.\
В отдельном терминале скопируйте файл внутрь контейнера, прямо в корневую директорию.\
В терминале с интерактивным режимом - выведите содержимое файла на экран.\
Пока что не останавливайте контейнер.
Великолепно! Теперь перейдём к запуску контейнера на основе этого образа. Для этого мы воспользуемся всемогущей командой `docker run`.
```
$ sudo docker run busybox
```
12. В терминале, отдельном от терминала контейнера с интерактивным режимом, сделайте листинг корневой директории контейнера.\
Также, с помощью одной из команд Docker, выведите на экран список изменений в файловой системе контейнера по сравнению\
с базовым образом.\
После этого, в этом же терминале, удалите из контейнера скопированный туда файл.\
Теперь интерактивный контейнер можно остановить.
Постойте, но ничего не произошло! Это баг? Ну, нет. Под капотом произошло много всего. Когда вы запустили команду `run`, клиент Docker нашёл образ (в нашем случае, `busybox`), загрузил контейнер и запустил команду внутри этого контейнера. Мы не указали никаких аргументов, так что контейнер загрузился, выполнил команду `sh` и процесс контейнера завершился. Ну, да, как-то обидно. Попробуем сделать что-нибудь поинтереснее.
```
$ sudo docker run busybox echo "hello from busybox"
hello from busybox
```
### 3. Volumes
Ура, наконец-то что-то вывелось. Теперь Docker запустил команду `echo` внутри контейнера, а затем вышел из него. Вы, наверное, заметили, что всё произошло очень быстро. А теперь представьте себе процесс загрузки виртуальной машины, выполнения в ней команды и её выключения. Ясно, почему говорят, что контейнеры быстрые! Чтобы узнать время выполнения попробуйте запустить последнюю команду со словом `time` в начале.
### 4. Сеть
Давайте взглянем на команду `docker ps`. Она выводит на экран список всех запущенных контейнеров.
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
В силу того, что ни один контейнер не запущен, выводится пустая строка. Попробуем более информативный вариант `docker ps -a`:
```
$ sudo 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 показывает, что эти контейнеры остановились несколько минут назад.
Наверное вы думаете, существует ли способ запустить более одной команды в контейнере. Давайте попробуем:
```
$ sudo 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 (можно несколько) из вывода выше и передайте параметрами в команду.
```
$ sudo docker rm 305297d7a235 ff0a5c3750b9
305297d7a235
ff0a5c3750b9
```
При удалении идентификаторы будут снова выведены на экран. Если нужно удалить много контейнеров, то вместо ручного копирования и вставки можно сделать так:
```
$ sudo 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`.
```
$ sudo docker run nginx:latest
```
Так как образа не существует локально, клиент сначала скачает образ из реестра, а потом запустит его. Если всё пройдёт без проблем, то вы увидите в терминале сообщения о запуске nginx. Теперь сервер запущен. Как увидеть сайт в действии? На каком порту работает сервер? И, что самое важное, как напрямую достучаться до контейнера из хост системы?
В нашем случае клиент не открывает никакие порты, так что нужно будет перезапустить команду `docker run` чтобы сделать порты публичными. Заодно давайте сделаем так, чтобы терминал не был прикреплен к запущенному контейнеру. В таком случае можно будет спокойно закрыть терминал, а контейнер продолжит работу. Этот режим называется `detached`.
```
$ sudo docker run -d -P --name static-site nginx:latest
e61d12292d69556eabe2a44c16cbd54486b2527e2ce4f95438e504afb7b02810
```
Флаг `-d` открепит (`--detach`) терминал, флаг `-P` сделает все открытые порты публичными и случайными, и, наконец, флаг `--name` это имя, которое мы хотим дать контейнеру. Теперь можно увидеть порты с помощью команды `docker port [CONTAINER]`.
```
$ sudo 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 клиент будет перенаправлять запросы на соединение к контейнеру.
```
$ sudo 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. Для этого задания подготовлено маленькое приложение `cats-app`, которое выводит случайную картинку с кошкой. Склонируйте этот репозиторий к себе на локальную машину `git clone <адрес-репозитория>` и перейдите в папку с приложением.
Следующим шагом является создание образа с данным веб-приложением. Как говорилось выше, все пользовательские образы базируются на базовых образах. Так как приложение написано на Python, базовый образ следует выбрать с предустановленным Python 3. Точнее мы собираемся использовать `python:3.8` версию python образа.
Теперь у нас есть все ингредиенты для создания собственных образов - работающее веб-приложение и базовый образ. Как мы будем подходить к этой задаче? Ответ — `Dockerfile`.
### 2.4 Dockerfile
**Dockerfile** — это простой текстовый файл, в котором содержится список команд Docker клиента. Это простой способ автоматизировать процесс создания образа. Самое классное, что команды в Dockerfile почти идентичны своим аналогам в Linux. Это значит, что в принципе не нужно изучать новый синтаксис, чтобы начать работать с докер-файлами.
В директории с приложением, нам нужно его создать. Создайте пустой файл в любимом текстовом редакторе, и сохраните его в той же директории `cats-app`. Назовите файл Dockerfile.
Для начала укажем базовый образ. Для этого нужно использовать ключевое слово `FROM`.
```Dockerfile
FROM python:3.8
```
Следующим шагом обычно указывают команды для копирования файлов из текущей папки в файловую систему образа и установки зависимостей.
```Dockerfile
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
```
Дальше нам нужно указать порт, который следует открыть. Наше приложение работает на порту 5000, поэтому укажем его:
```Dockerfile
EXPOSE 5000
```
Последний шаг — указать команду по-умолчанию для запуска приложения. Это просто `python ./app.py`. Для этого используем команду `CMD`:
```Dockerfile
CMD ["python", "./app.py"]
```
Главное предназначение `CMD` — это сообщить контейнеру какие команды нужно выполнить при старте. Теперь наш `Dockerfile` готов. Вот как он выглядит:
```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`.
Листинг ниже демонстрирует процесс. Перед тем, как запустите команду сами (не забудьте путь к `cats-app` в конце), проверьте, чтобы там был ваш `имя_пользователя`. Имя пользователя должено соответствовать тому, что использовалось при регистрации на Docker Hub. Если вы используете частный реестр, то к тэгу добавляется в начало доменное имя хоста и опциональный порт, например
```Dockerfile
# -t доменное_имя_реестра:порт/имя_пользователя/имя_образа:тэг_образа
-t myregistryhost:5000/fedora/httpd:version1.0
```
Команда `docker build` довольно проста: она принимает опциональный тег с флагом `-t имя_пользователя/имя_образа` и путь до директории, в которой лежит `Dockerfile`.
```
$ docker build -t studX/catsapp ./
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
Successfully tagged studX/catsapp:latest
```
Если у вас нет образа `python:3.8`, то клиент сначала скачает его, а потом возьмётся за создание вашего образа. Так что, вывод на экран может отличаться от приведённого выше. Если всё прошло хорошо, то образ готов! Запустите `docker images` и увидите свой образ в списке.
Последний шаг — запустить образ и проверить его работоспособность:
```
$ sudo docker run -p 80:5000 --name catsapp studX/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, называется `foodtrucks-app`. Приложение создавалось с целью сделать что-то похожее на реально эксплуатируемое приложение и приносещее пользу, но не слишком сложное.
Серверная часть написана на 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 Гб. Если вы увеличили оббъём памяти в виртуальной машине, не забудьте остановить и запустить виртуальную машину, чтобы изменения применились.
Проблема также может быть в максимальное количество кусочков памяти `vm.max_map_count`, которые может иметь процесс . Увеличьте количество доступных кусочков памяти процессу выполнением команды `sysctl -w vm.max_map_count=262144`. Обратите внимание, что это модифицирует свойство системы, в которой может выполняться множество других контейнеров.
```
$ sudo docker run -d -p 9200:9200 --name elastic elasticsearch:8.4.3
d582e031a005f41eea704cdc6b21e62e7a8a42021297ce7ce123b945ae3d3763
```
Проверьте, что в логах нет ошибок.
```
$ sudo docker logs elastic
```
При запуске в режиме демона сервис не генерирует автоматически пароль для пользователя `elastic`. Мы можем сделать это сами после запуска, чтобы протестировать сервис.
```
$ sudo 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. В связи с появлением дополнительных файлов для работы контейнера нам нужно построить новый образ. Начнем с базового образа `ubuntu:focal`.
**Замечание.** Если оказывается, что существующий образ не подходит для вашей задачи, то спокойно создавайте свой образ на основе другого базового образа. В большинстве случаев, для образов на Docker Hub можно найти соответствующий Dockerfile на Github. Почитайте существующие докерфайлы — это один из лучших способов научиться делать свои образы.
Наш Dockerfile для приложения `foodtrucks-app` выглядит следующим образом:
```
# 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. В нашем случае это будет параметры elasticsearch сервера: протокол, доменное имя, порт, имя пользователя, пароль.
Наконец, можно собрать образ и запустить контейнер.
```
$ sudo docker build -t studX/foodtrucks-web.
```
При первом запуске нужно будет больше времени, так как клиент Докера будет скачивать образ ubuntu, запускать все команды и готовить образ. Повторный запуск `docker build` после последующих изменений будет практически моментальным. Давайте попробуем запустить приложение.
```
$ docker run -P studX/foodtrucks-web https localhost 9200 elastic 36EBVQtPjbiFPMh9Bk7X
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. Что тут у нас:
```
$ sudo 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
```
Итак, у нас есть контейнер `elastic`, который слушает по любому локальному адресу (0.0.0.0) и порту 9200, и мы можем напрямую обращаться к нему с хоста. Если можно было бы сообщить нашему приложению как подключиться к этому адресу, то оно сможет общаться с `elastic`, верно? Давайте взглянем на код на Python, туда, где описано подключение и вспомним, переданные в контейнер аргументы.
```python
es_scheme = sys.argv[1]
es_host = sys.argv[2]
es_port = int(sys.argv[3])
es_user = sys.argv[4]
es_password = sys.argv[5]
connection_string = f"{es_scheme}://{es_host}:{es_port}"
es = Elasticsearch(
connection_string,
basic_auth=(es_user, es_password),
verify_certs=False
)
```
```
https localhost 9200 elastic 36EBVQtPjbiFPMh9Bk7X
```
Для того, чтобы это заработало, нужно запустить `foodttrucks-web` контейнер на том же хосте, что и контейнер `elastic`, и всё заработает, да? К сожалению, нет, потому что контейнер `elastic` доступен по адресу хост-машины только с хост-машины. Другой контейнер не сможет обратиться по этому адресу. Ладно, если не этот адрес, то какой тогда адрес нужно использовать для работы с контейнером `elastic`? Хорошо, что вы спросили.
Подошло время, чтобы изучить работу сети в Docker. После установки, Docker автоматически создает три сети:
```
$ sudo docker network ls
NETWORK ID NAME DRIVER
075b9f628ccc none null
be0f7178486c host host
8022115322ec bridge bridge
```
Сеть bridge — это сеть, в которой контейнеры запущены по умолчанию (программный роутер). Это значит, что когда вы запускаете контейнер `elastic`, он работает в bridge сети. Чтобы удостовериться, давайте проверим:
```
$ sudo 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`. Именно этот адрес мы и искали? Давайте проверим: запустим `foodtrucks-web` приложение и попробуем обратиться к нему по IP:
```
$ sudo docker run -it --rm studX/foodtrucks-web --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` на самом деле можно обращаться к `elastic`! Супер!
Не смотря на то, что мы нашли способ наладить связь между контейнерами, существует несколько проблем с этим подходом.
Придется добавлять записи в файл `/etc/hosts` (локальный DNS) внутри `foodtrucks-web` контейнера, чтобы приложение понимало, что имя хоста `elastic` означает `172.17.0.2`. Если IP-адрес меняется, то придется вручную менять запись.
Так как сеть bridge используется всеми контейнерами по умолчанию, этот метод не безопасен.
Но есть хорошие новости: в Docker есть отличное решение этой проблемы. Docker позволяет создавать собственные изолированные сети. Это решение также помогает справиться с проблемой `/etc/hosts`, сейчас увидим как.
Во-первых, давайте создадим свою сеть.
```
$ sudo docker network create foodtrucks-network
1a3386375797001999732cb4c4e97b88172d983b08cd0addfcb161eed0c18d89
```
```
$ sudo 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` по умолчанию.
```
$ sudo 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
```
```
$ sudo docker stop e931ab24dedc
e931ab24dedc
$ sudo docker rm e931ab24dedc
e931ab24dedc
```
```
$ sudo docker run -d -p 9200:9200 --net foodtrucks-network --name elastic elasticsearch:8.4.3
2c0b96f9b8030f038e40abea44c2d17b0a8edda1354a08166c33e6d351d0c651
```
Сгенерируем пароль для Elasticsearch, как делали ранее:
```
$ sudo docker exec -ti elastic /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
...
Password for the [elastic] user successfully reset.
New value: 9gllhwpGufZq2GfkfZ9c
```
Мы сделали то же, что и раньше, но на этот раз присоединили контейнер к сети `foodtrucks-network`.
```
$ sudo 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": {}
}
]
```
Перед тем, как запускать контейнер с приложением, давайте проверим что происходит, когда запуск происходит в сети.
```
$ sudo docker run -it --rm --net foodtrucks-network --entrypoint /bin/bash studX/foodtrucks-web
root@53af252b771a:/opt/flask-app# curl https://elastic:9200 --insecure --user elastic:9gllhwpGufZq2GfkfZ9c
{
"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
...
Total trucks loaded: 478
* 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.21.0.3:5000
Press CTRL+C to quit
```
```
root@53af252b771a:/opt/flask-app# exit
```
Ура! Работает! Магическим образом Docker теперь разрешает имя `elastic` в нужный ip, и поэтому `https://elastic:9200` можно использовать в приложении — этот адрес корректно направляет запросы в контейнер `elastic`. Отлично! Давайте теперь запустим `foodtrucks-web` контейнер по-настоящему:
```
$ sudo docker run -d --net foodtrucks-network -p 80:5000 --name foodtrucks-web studX/foodtrucks-web
2a1b77e066e646686f669bab4759ec1611db359362a031667cacbe45c3ddb413
```
```
$ sudo 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 elastic
```
Зайдите на 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, specify password beforehand
docker run -d --net foodtrucks-network --name elastic -e ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS 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/.
```
$ sudo curl -SL https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
```
Проверить работоспособность так:
```
$ sudo docker-compose version
Docker Compose version v2.12.2
```
Теперь можно перейти к следующему шагу, то есть к созданию файла `docker-compose.yml`. Синтаксис yml-файлов очень простой (но отступы коварны):
```yaml
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. Для `foodtrucks-web` приложения — тот образ, который мы создали самостоятельно до этого.
С помощью других параметров вроде `command` и `ports` можно предоставить информацию о контейнере. Подробнее о параметрах и возможных значениях можно прочитать в документации.
**Замечание.** Нужно находиться в директории с файлом `docker-compose.yml` чтобы запускать большую часть команд Compose.
Отлично! Файл готов, давайте посмотрим на `docker-compose` в действии. Но вначале нужно удостовериться, что порты свободны. Так что если у вас запущены контейнеры `foodtrucks-web` и `elastic`, то пора их остановить:
```
$ sudo docker stop $(docker ps -q)
39a2f5df14ef
2a1b77e066e6
```
Теперь можно запускать `docker-compose`. Перейдите в директорию с приложением Foodtrucks и выполните команду `docker-compose up`.
```
$ sudo 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
```
```
$ sudo docker-compose up -d
[+] Running 2/2
⠿ Container elastic Started 0.7s
⠿ Container foodtrucks-web Started 1.4s
```
```
$ sudo 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
```
Проверим, создались ли какие-нибудь сети:
```
$ sudo 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`.
```
$ sudo 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
```
```
$ sudo 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, запускать свои контейнеры, запускать статические и динамические веб-сайты.
## Релевантные источники
- https://github.com/prakhar1989/docker-curriculum
- Nemeth E. et al. UNIX and Linux system administration handbook. Chapter 25.
### 5. Создание образов

@ -0,0 +1,905 @@
# Задания
## 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
```
Проверьте, всё ли установлено корректно:
```
$ sudo docker run hello-world
Hello from Docker.
This message shows that your installation appears to be working correctly.
```
## 2. Играем с BusyBox
Теперь, когда всё подготовлено, пора приняться за дело. В этом разделе нашей целью будет запуск контейнера Busybox и освоение команды docker run.
Для начала, запустите следующую команду:
```
$ sudo docker pull busybox
```
**Внимание.** В зависимости от того, как вы установили Docker, вы можете увидеть сообщение permission denied (доступ запрещён) в ответ на вызов выше приведённой команды. Если вы на Mac, убедитесь, что Docker движок запущен. Если на Линукс, вам может потребоваться повысить права доступа с помощью команды `sudo`. В качестве альтернативного варианта вы можете добавить пользователя в Docker группу для решения этой проблемы.
Команда `pull` скачивает образ `busybox` из Docker реестра и сохраняет его в систему. Вы можете использовать команду `docker images` для вывода в консоль списка образов находящихся в вашей системе.
```
$ sudo 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`.
```
$ sudo docker run busybox
```
Постойте, но ничего не произошло! Это баг? Ну, нет. Под капотом произошло много всего. Когда вы запустили команду `run`, клиент Docker нашёл образ (в нашем случае, `busybox`), загрузил контейнер и запустил команду внутри этого контейнера. Мы не указали никаких аргументов, так что контейнер загрузился, выполнил команду `sh` и процесс контейнера завершился. Ну, да, как-то обидно. Попробуем сделать что-нибудь поинтереснее.
```
$ sudo docker run busybox echo "hello from busybox"
hello from busybox
```
Ура, наконец-то что-то вывелось. Теперь Docker запустил команду `echo` внутри контейнера, а затем вышел из него. Вы, наверное, заметили, что всё произошло очень быстро. А теперь представьте себе процесс загрузки виртуальной машины, выполнения в ней команды и её выключения. Ясно, почему говорят, что контейнеры быстрые! Чтобы узнать время выполнения попробуйте запустить последнюю команду со словом `time` в начале.
Давайте взглянем на команду `docker ps`. Она выводит на экран список всех запущенных контейнеров.
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
В силу того, что ни один контейнер не запущен, выводится пустая строка. Попробуем более информативный вариант `docker ps -a`:
```
$ sudo 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 показывает, что эти контейнеры остановились несколько минут назад.
Наверное вы думаете, существует ли способ запустить более одной команды в контейнере. Давайте попробуем:
```
$ sudo 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 (можно несколько) из вывода выше и передайте параметрами в команду.
```
$ sudo docker rm 305297d7a235 ff0a5c3750b9
305297d7a235
ff0a5c3750b9
```
При удалении идентификаторы будут снова выведены на экран. Если нужно удалить много контейнеров, то вместо ручного копирования и вставки можно сделать так:
```
$ sudo 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`.
```
$ sudo docker run nginx:latest
```
Так как образа не существует локально, клиент сначала скачает образ из реестра, а потом запустит его. Если всё пройдёт без проблем, то вы увидите в терминале сообщения о запуске nginx. Теперь сервер запущен. Как увидеть сайт в действии? На каком порту работает сервер? И, что самое важное, как напрямую достучаться до контейнера из хост системы?
В нашем случае клиент не открывает никакие порты, так что нужно будет перезапустить команду `docker run` чтобы сделать порты публичными. Заодно давайте сделаем так, чтобы терминал не был прикреплен к запущенному контейнеру. В таком случае можно будет спокойно закрыть терминал, а контейнер продолжит работу. Этот режим называется `detached`.
```
$ sudo docker run -d -P --name static-site nginx:latest
e61d12292d69556eabe2a44c16cbd54486b2527e2ce4f95438e504afb7b02810
```
Флаг `-d` открепит (`--detach`) терминал, флаг `-P` сделает все открытые порты публичными и случайными, и, наконец, флаг `--name` это имя, которое мы хотим дать контейнеру. Теперь можно увидеть порты с помощью команды `docker port [CONTAINER]`.
```
$ sudo 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 клиент будет перенаправлять запросы на соединение к контейнеру.
```
$ sudo 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. Для этого задания подготовлено маленькое приложение `cats-app`, которое выводит случайную картинку с кошкой. Склонируйте этот репозиторий к себе на локальную машину `git clone <адрес-репозитория>` и перейдите в папку с приложением.
Следующим шагом является создание образа с данным веб-приложением. Как говорилось выше, все пользовательские образы базируются на базовых образах. Так как приложение написано на Python, базовый образ следует выбрать с предустановленным Python 3. Точнее мы собираемся использовать `python:3.8` версию python образа.
Теперь у нас есть все ингредиенты для создания собственных образов - работающее веб-приложение и базовый образ. Как мы будем подходить к этой задаче? Ответ — `Dockerfile`.
### 2.4 Dockerfile
**Dockerfile** — это простой текстовый файл, в котором содержится список команд Docker клиента. Это простой способ автоматизировать процесс создания образа. Самое классное, что команды в Dockerfile почти идентичны своим аналогам в Linux. Это значит, что в принципе не нужно изучать новый синтаксис, чтобы начать работать с докер-файлами.
В директории с приложением, нам нужно его создать. Создайте пустой файл в любимом текстовом редакторе, и сохраните его в той же директории `cats-app`. Назовите файл Dockerfile.
Для начала укажем базовый образ. Для этого нужно использовать ключевое слово `FROM`.
```Dockerfile
FROM python:3.8
```
Следующим шагом обычно указывают команды для копирования файлов из текущей папки в файловую систему образа и установки зависимостей.
```Dockerfile
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
```
Дальше нам нужно указать порт, который следует открыть. Наше приложение работает на порту 5000, поэтому укажем его:
```Dockerfile
EXPOSE 5000
```
Последний шаг — указать команду по-умолчанию для запуска приложения. Это просто `python ./app.py`. Для этого используем команду `CMD`:
```Dockerfile
CMD ["python", "./app.py"]
```
Главное предназначение `CMD` — это сообщить контейнеру какие команды нужно выполнить при старте. Теперь наш `Dockerfile` готов. Вот как он выглядит:
```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`.
Листинг ниже демонстрирует процесс. Перед тем, как запустите команду сами (не забудьте путь к `cats-app` в конце), проверьте, чтобы там был ваш `имя_пользователя`. Имя пользователя должено соответствовать тому, что использовалось при регистрации на Docker Hub. Если вы используете частный реестр, то к тэгу добавляется в начало доменное имя хоста и опциональный порт, например
```Dockerfile
# -t доменное_имя_реестра:порт/имя_пользователя/имя_образа:тэг_образа
-t myregistryhost:5000/fedora/httpd:version1.0
```
Команда `docker build` довольно проста: она принимает опциональный тег с флагом `-t имя_пользователя/имя_образа` и путь до директории, в которой лежит `Dockerfile`.
```
$ docker build -t studX/catsapp ./
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
Successfully tagged studX/catsapp:latest
```
Если у вас нет образа `python:3.8`, то клиент сначала скачает его, а потом возьмётся за создание вашего образа. Так что, вывод на экран может отличаться от приведённого выше. Если всё прошло хорошо, то образ готов! Запустите `docker images` и увидите свой образ в списке.
Последний шаг — запустить образ и проверить его работоспособность:
```
$ sudo docker run -p 80:5000 --name catsapp studX/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, называется `foodtrucks-app`. Приложение создавалось с целью сделать что-то похожее на реально эксплуатируемое приложение и приносещее пользу, но не слишком сложное.
Серверная часть написана на 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 Гб. Если вы увеличили оббъём памяти в виртуальной машине, не забудьте остановить и запустить виртуальную машину, чтобы изменения применились.
Проблема также может быть в максимальное количество кусочков памяти `vm.max_map_count`, которые может иметь процесс . Увеличьте количество доступных кусочков памяти процессу выполнением команды `sysctl -w vm.max_map_count=262144`. Обратите внимание, что это модифицирует свойство системы, в которой может выполняться множество других контейнеров.
```
$ sudo docker run -d -p 9200:9200 --name elastic elasticsearch:8.4.3
d582e031a005f41eea704cdc6b21e62e7a8a42021297ce7ce123b945ae3d3763
```
Проверьте, что в логах нет ошибок.
```
$ sudo docker logs elastic
```
При запуске в режиме демона сервис не генерирует автоматически пароль для пользователя `elastic`. Мы можем сделать это сами после запуска, чтобы протестировать сервис.
```
$ sudo 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. В связи с появлением дополнительных файлов для работы контейнера нам нужно построить новый образ. Начнем с базового образа `ubuntu:focal`.
**Замечание.** Если оказывается, что существующий образ не подходит для вашей задачи, то спокойно создавайте свой образ на основе другого базового образа. В большинстве случаев, для образов на Docker Hub можно найти соответствующий Dockerfile на Github. Почитайте существующие докерфайлы — это один из лучших способов научиться делать свои образы.
Наш Dockerfile для приложения `foodtrucks-app` выглядит следующим образом:
```
# 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. В нашем случае это будет параметры elasticsearch сервера: протокол, доменное имя, порт, имя пользователя, пароль.
Наконец, можно собрать образ и запустить контейнер.
```
$ sudo docker build -t studX/foodtrucks-web.
```
При первом запуске нужно будет больше времени, так как клиент Докера будет скачивать образ ubuntu, запускать все команды и готовить образ. Повторный запуск `docker build` после последующих изменений будет практически моментальным. Давайте попробуем запустить приложение.
```
$ docker run -P studX/foodtrucks-web https localhost 9200 elastic 36EBVQtPjbiFPMh9Bk7X
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. Что тут у нас:
```
$ sudo 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
```
Итак, у нас есть контейнер `elastic`, который слушает по любому локальному адресу (0.0.0.0) и порту 9200, и мы можем напрямую обращаться к нему с хоста. Если можно было бы сообщить нашему приложению как подключиться к этому адресу, то оно сможет общаться с `elastic`, верно? Давайте взглянем на код на Python, туда, где описано подключение и вспомним, переданные в контейнер аргументы.
```python
es_scheme = sys.argv[1]
es_host = sys.argv[2]
es_port = int(sys.argv[3])
es_user = sys.argv[4]
es_password = sys.argv[5]
connection_string = f"{es_scheme}://{es_host}:{es_port}"
es = Elasticsearch(
connection_string,
basic_auth=(es_user, es_password),
verify_certs=False
)
```
```
https localhost 9200 elastic 36EBVQtPjbiFPMh9Bk7X
```
Для того, чтобы это заработало, нужно запустить `foodttrucks-web` контейнер на том же хосте, что и контейнер `elastic`, и всё заработает, да? К сожалению, нет, потому что контейнер `elastic` доступен по адресу хост-машины только с хост-машины. Другой контейнер не сможет обратиться по этому адресу. Ладно, если не этот адрес, то какой тогда адрес нужно использовать для работы с контейнером `elastic`? Хорошо, что вы спросили.
Подошло время, чтобы изучить работу сети в Docker. После установки, Docker автоматически создает три сети:
```
$ sudo docker network ls
NETWORK ID NAME DRIVER
075b9f628ccc none null
be0f7178486c host host
8022115322ec bridge bridge
```
Сеть bridge — это сеть, в которой контейнеры запущены по умолчанию (программный роутер). Это значит, что когда вы запускаете контейнер `elastic`, он работает в bridge сети. Чтобы удостовериться, давайте проверим:
```
$ sudo 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`. Именно этот адрес мы и искали? Давайте проверим: запустим `foodtrucks-web` приложение и попробуем обратиться к нему по IP:
```
$ sudo docker run -it --rm studX/foodtrucks-web --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` на самом деле можно обращаться к `elastic`! Супер!
Не смотря на то, что мы нашли способ наладить связь между контейнерами, существует несколько проблем с этим подходом.
Придется добавлять записи в файл `/etc/hosts` (локальный DNS) внутри `foodtrucks-web` контейнера, чтобы приложение понимало, что имя хоста `elastic` означает `172.17.0.2`. Если IP-адрес меняется, то придется вручную менять запись.
Так как сеть bridge используется всеми контейнерами по умолчанию, этот метод не безопасен.
Но есть хорошие новости: в Docker есть отличное решение этой проблемы. Docker позволяет создавать собственные изолированные сети. Это решение также помогает справиться с проблемой `/etc/hosts`, сейчас увидим как.
Во-первых, давайте создадим свою сеть.
```
$ sudo docker network create foodtrucks-network
1a3386375797001999732cb4c4e97b88172d983b08cd0addfcb161eed0c18d89
```
```
$ sudo 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` по умолчанию.
```
$ sudo 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
```
```
$ sudo docker stop e931ab24dedc
e931ab24dedc
$ sudo docker rm e931ab24dedc
e931ab24dedc
```
```
$ sudo docker run -d -p 9200:9200 --net foodtrucks-network --name elastic elasticsearch:8.4.3
2c0b96f9b8030f038e40abea44c2d17b0a8edda1354a08166c33e6d351d0c651
```
Сгенерируем пароль для Elasticsearch, как делали ранее:
```
$ sudo docker exec -ti elastic /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
...
Password for the [elastic] user successfully reset.
New value: 9gllhwpGufZq2GfkfZ9c
```
Мы сделали то же, что и раньше, но на этот раз присоединили контейнер к сети `foodtrucks-network`.
```
$ sudo 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": {}
}
]
```
Перед тем, как запускать контейнер с приложением, давайте проверим что происходит, когда запуск происходит в сети.
```
$ sudo docker run -it --rm --net foodtrucks-network --entrypoint /bin/bash studX/foodtrucks-web
root@53af252b771a:/opt/flask-app# curl https://elastic:9200 --insecure --user elastic:9gllhwpGufZq2GfkfZ9c
{
"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
...
Total trucks loaded: 478
* 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.21.0.3:5000
Press CTRL+C to quit
```
```
root@53af252b771a:/opt/flask-app# exit
```
Ура! Работает! Магическим образом Docker теперь разрешает имя `elastic` в нужный ip, и поэтому `https://elastic:9200` можно использовать в приложении — этот адрес корректно направляет запросы в контейнер `elastic`. Отлично! Давайте теперь запустим `foodtrucks-web` контейнер по-настоящему:
```
$ sudo docker run -d --net foodtrucks-network -p 80:5000 --name foodtrucks-web studX/foodtrucks-web
2a1b77e066e646686f669bab4759ec1611db359362a031667cacbe45c3ddb413
```
```
$ sudo 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 elastic
```
Зайдите на 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, specify password beforehand
docker run -d --net foodtrucks-network --name elastic -e ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS 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/.
```
$ sudo curl -SL https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
```
Проверить работоспособность так:
```
$ sudo docker-compose version
Docker Compose version v2.12.2
```
Теперь можно перейти к следующему шагу, то есть к созданию файла `docker-compose.yml`. Синтаксис yml-файлов очень простой (но отступы коварны):
```yaml
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. Для `foodtrucks-web` приложения — тот образ, который мы создали самостоятельно до этого.
С помощью других параметров вроде `command` и `ports` можно предоставить информацию о контейнере. Подробнее о параметрах и возможных значениях можно прочитать в документации.
**Замечание.** Нужно находиться в директории с файлом `docker-compose.yml` чтобы запускать большую часть команд Compose.
Отлично! Файл готов, давайте посмотрим на `docker-compose` в действии. Но вначале нужно удостовериться, что порты свободны. Так что если у вас запущены контейнеры `foodtrucks-web` и `elastic`, то пора их остановить:
```
$ sudo docker stop $(docker ps -q)
39a2f5df14ef
2a1b77e066e6
```
Теперь можно запускать `docker-compose`. Перейдите в директорию с приложением Foodtrucks и выполните команду `docker-compose up`.
```
$ sudo 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
```
```
$ sudo docker-compose up -d
[+] Running 2/2
⠿ Container elastic Started 0.7s
⠿ Container foodtrucks-web Started 1.4s
```
```
$ sudo 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
```
Проверим, создались ли какие-нибудь сети:
```
$ sudo 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`.
```
$ sudo 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
```
```
$ sudo 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, запускать свои контейнеры, запускать статические и динамические веб-сайты.
## Релевантные источники
- https://github.com/prakhar1989/docker-curriculum
- Nemeth E. et al. UNIX and Linux system administration handbook. Chapter 25.

@ -0,0 +1,9 @@
## Инструменты Docker
### 1. Многоконтейнерные приложения
Docker Compose — это инструмент для запуска многоконтейнерных приложений в Docker, определенных с использованием формата файлов Compose. Файл Compose используется для определения того, как настроены один или несколько контейнеров, составляющих ваше приложение. Когда у вас есть файл Compose, вы можете создать и запустить приложение с помощью одной команды: docker compose up. В качестве типичного примера приложения можно привести веб-сайт, состоящий из контейнера фронтэнда и контейнера бэкенда.
### 2. Кластерные приложения
Docker Swarm это простой оркестратор для контейнеров, который доступен из коробки. Позволяет объединить докер демоны на разных машинах в кластер, что даёт возможность поставки распределённых приложений упакованных в контейнеры. Swarm, так же как и Compose, имеет декларативную модель описания кластерного приложения. Типичными задачами для такого типа приложений являются организация высокой доступности, балансировки нагрузки и канареечного обновления сервиса. Также вы можете добиться эластичности распределённого приложения, которое создаёт и удаляет контейнеры в зависимости от поступающей нагрузки.

@ -0,0 +1,2 @@
1. Для чего используется инструмент docker compose?
Loading…
Cancel
Save