Перейти к основному содержимому

Мастер-класс по Docker

Часть 0. Подготовка

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

Часть 1. Что такое Docker

Основано на материале Как работает Docker: подробный гайд от техлида | Skillbox

Docker — это платформа, которая позволяет упаковать в контейнер приложение со всем окружением и зависимостями, а затем доставить и запустить его в целевой системе.

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

Устройство и принцип работы Docker

Виртуализация в Docker реализуется на уровне ОС. Виртуальная среда запускается прямо из ядра основной операционной системы и использует её ресурсы.

docker_architecture

В поставку Docker входят следующие компоненты:

  • Docker host — это операционная система, на которую устанавливают Docker и на которой он работает.
  • Docker daemon — служба, которая управляет Docker-объектами: сетями, хранилищами, образами и контейнерами.
  • Docker client — консольный клиент, при помощи которого пользователи взаимодействуют с Docker daemon и отправляют ему команды, создают контейнеры и управляют ими.
  • Docker image — это неизменяемый образ, из которого разворачивается контейнер.
  • Docker container — развёрнутое и запущенное приложение.
  • Docker Registry — репозиторий, в котором хранятся образы.
  • Dockerfile — файл-инструкция для сборки образа.
  • Docker Compose — инструмент для управления несколькими контейнерами. Он позволяет создавать контейнеры и задавать их конфигурацию.
  • Docker Desktop — GUI-клиент, который распространяется по GPL. Бесплатная версия работает на Windows, macOS, а с недавних пор и на Linux. Это очень удобный клиент, который отображает все сущности Docker и позволяет запустить однонодовый Kubernetes для компьютера.

Docker изначально создавался под Linux. Поэтому на Windows и macOS запускают виртуальную машину с Linux, а поверх неё — Docker. В macOS используют VirtualBox или QEMU, а в Windows — Hyper-V.

Схемы архитектуры Docker

docker_scheme

Работа поверх виртуалок повышает потребление ресурсов. Поэтому Docker на macOS и Windows работает медленнее и с рядом ограничений. Для разработки это приемлемо, но "в бою" так делать никто не будет. На всех популярных платформах в проде используют Linux.

Чем виртуализация отличается от контейнеризации

Контейнеры и виртуальные машины — это разные способы виртуализации. Только виртуалка реализует её на уровне железа, а Docker — на уровне операционной системы.

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

containers-vs-virtual-machines

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

Сущности Docker

Docker работает с несколькими сущностями:

  • Docker image (образ). Это шаблон, по которому создают контейнеры. Его часто сравнивают со слоёным пирогом: мы накладываем слой файловой системы поверх слоя базового образа и получаем неизменяемый образ. В него можно установить приложение, конфигурации и зависимости. Другие образы могут наследоваться, поэтому если положить сверху слой файлов и закоммитить, то мы получим ещё один неизменяемый образ.

    docker-image

  • Dockerfile. Если Docker image — это пирог, то Dockerfile — рецепт его приготовления. В этом файле описаны основные инструкции для сборки образа: какой базовый образ взять, откуда и куда положить файлы и так далее.

  • Контейнер — это runtime-сущность на основе образа, приложение, которое мы развернули с помощью Docker. Можно провести такую аналогию: образ — это инсталлятор программы, а контейнер — уже запущенная программа.

    При развёртывании контейнера поверх файловой системы создаётся ещё один изменяемый слой. Приложение внутри контейнера может записывать туда данные или редактировать их. После удаления контейнера данные стираются, но их можно сохранить с помощью volumes.

  • Docker Registry. Это репозиторий, в котором хранятся Docker-образы. Он может быть как локальным, так и публичным. Репозитории создают на платформах вроде Docker Hub и GitLab и размещают в них образы с описанием, разными версиями и тегами.

docker-demon

Часть 2. Установка Docker на Ubuntu

Изучить актуальный способ установки в статье Установка Docker Engine в Ubuntu.

Альтернативный способ установки можно узнать здесь.

Часть 3. Команды управления контейнерами и образами

Основано на статьях:

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

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

sudo docker pull busybox

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

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

$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest a416a98b71e2 2 months ago 4.26MB

Docker Run

Отлично! Теперь давайте запустим Docker-контейнер с этим образом. Для этого используем волшебную команду docker run:

sudo docker run busybox

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

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

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

Теперь давайте взглянем на команду 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
71d2473b229b busybox "echo 'hello from bu…" 44 seconds ago Exited (0) 44 seconds ago vibrant_burnell
bb2b610d6370 busybox "sh" About a minute ago Exited (0) About a minute ago kind_tesla

Теперь виден список всех контейнеров, которые мы запускали. В колонке STATUS можно заметить, что контейнеры завершили свою работу несколько минут назад.

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

$ sudo docker run -it busybox sh
/ # ls
bin dev etc home lib lib64 proc root sys tmp usr var
/ # uptime
22:51:29 up 5 min, 0 users, load average: 0.00, 0.00, 0.00

Команда run с флагом -it подключает интерактивный tty в контейнер. Теперь можно запускать сколько угодно много команд внутри. Попробуйте.

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

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

$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
830d6d332ef5 busybox "sh" About a minute ago Exited (0) 7 seconds ago nice_matsumoto
71d2473b229b busybox "echo 'hello from bu…" 2 minutes ago Exited (0) 2 minutes ago vibrant_burnell
bb2b610d6370 busybox "sh" 3 minutes ago Exited (0) 3 minutes ago kind_tesla
$ sudo docker rm 830d6d332ef5 71d2473b229b bb2b610d6370
830d6d332ef5
71d2473b229b
bb2b610d6370

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

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

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

Также можно удалять ненужные образы командой docker rmi:

$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest a416a98b71e2 2 months ago 4.26MB
aladin@ubuntu:~$ sudo docker rmi busybox
Untagged: busybox:latest
Untagged: busybox@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79
Deleted: sha256:a416a98b71e224a31ee99cff8e16063554498227d2b696152a9c3e0aa65e5824
Deleted: sha256:3d24ee258efc3bfe4066a1a9fb83febf6dc0b1548dfe896161533668281c9f4f

Часть 4. Упаковка приложения в контейнер

Статические сайты

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

Поехали. Для одностраничного сайта нам понадобится образ, который я заранее создал для этого пособия и разместил в регистре - prakhar1989/static-site. Можно скачать образ напрямую командой docker run.

sudo docker run prakhar1989/static-site

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

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

Замените <ФАМИЛИЯ> на свою фамилию, чтобы не конкурировать при выполнении задания с другими студентами.

$ sudo docker run -d -P --name static-site-<ФАМИЛИЯ> prakhar1989/static-site
7d4933779bb06678b3f65c2ebcb18d65d52248c4e41e2992f369e884689af2fb

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

$ sudo docker port static-site-<ФАМИЛИЯ>
80/tcp -> 0.0.0.0:32769
80/tcp -> [::]:32769
443/tcp -> 0.0.0.0:32768
443/tcp -> [::]:32768

Откройте http://<IP АДРЕС УДАЛЕННОГО УЗЛА>:32769 в своем браузере.

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

sudo apt install net-tools
netstat -lntu

Запустите контейнер:

$ sudo docker run -p 8888:80 prakhar1989/static-site
Nginx is running...

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

Образы

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

$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
prakhar1989/static-site latest f01030e1dcf3 7 years ago 134MB

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

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

sudo docker pull python:3.12

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

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

  • Base images (базовые образы) — это образы, которые не имеют родительского образа. Обычно это образы с операционной системой, такие как ubuntu, busybox или debian.
  • Child images (дочерние образы) — это образы, построенные на базовых образах и обладающие дополнительной функциональностью.

docker-kernel

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

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

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

Теперь, когда мы лучше понимаем, что такое образы и какие они бывают, самое время создать собственный образ. Цель этого раздела — создать образ с простым приложением на Flask.

Установим Flask:

sudo apt update
sudo apt -y install python3-pip
python3 -m pip install flask

Создадим файл:

mkdir ~/flaskapp
cd ~/flaskapp
~/flaskapp$ nano main.py

Файл main.py приложения будет выглядеть следующим образом:

import os
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
if __name__ == "__main__":
port = int(os.environ.get('PORT', 5000))
app.run(debug=True, host='0.0.0.0', port=port)

Подберите нужный порт, если порт 8000 занят.

$ export PORT=8000
$ python3 -m flask --app main run
* Serving Flask app 'main'
* 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 http://127.0.0.1:8000
Press CTRL+C to quit

Если все хорошо, то вы увидите вывод как в примере выше.

Создадим файл requirements.txt.

~/flaskapp$ nano requirements.txt

Содержимое файла requirements.txt:

Flask==3.0.0

Выглядит отлично, правда? Теперь нужно создать образ с приложением. Как говорилось выше, все пользовательские образы основаны на базовом образе. Так как наше приложение написано на Питоне, нам нужен базовый образ Python 3. В частности, нам нужна версия python:3.12-alpine базового образа с Питоном.

Создадим Dockerfile:

~/flaskapp$ nano Dockerfile

Следующий фрагмент основан Build and deploy a Flask app using Docker | LogRocket.

Содержимое файла Dockerfile:

FROM python:3.11-alpine
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
COPY . /app
EXPOSE 5000
CMD ["python", "main.py" ]

Давайте ознакомимся с инструкциями в этом файле Dockerfile:

  • FROM python:3.11-alpine: поскольку Docker позволяет нам наследовать существующие образы, мы устанавливаем образ Python и устанавливаем его в наш образ Docker. Alpine это облегченный дистрибутив Linux, который будет служить операционной системой, на которую мы устанавливаем наш образ.
  • COPY ./requirements.txt /app/requirements.txt: здесь мы копируем requirements файл и его содержимое (сгенерированные пакеты и зависимости) в app папку образа.
  • WORKDIR /app: мы переходим к установке рабочего каталога как /app, который будет корневым каталогом нашего приложения в контейнере.
  • RUN pip install -r requirements.txt: эта команда устанавливает все зависимости, определенные в requirements.txt файле, в наше приложение внутри контейнера.
  • COPY . /app: при этом все остальные файлы и их соответствующее содержимое будут скопированы в app папку, которая является корневым каталогом нашего приложения внутри контейнера.
  • EXPOSE 5000: указываем порт, который следует открыть. Наше приложение работает на порту 5000.
  • CMD ["python", "main.py" ]: последний шаг — указать команду для запуска приложения.

В итоге в рабочей папке должно получиться:

~/flaskapp$ ls
Dockerfile main.py requirements.txt
aladin@ubuntu:~/flaskapp$

Давайте перейдем к созданию образа с помощью приведенной ниже команды. Замените <ФАМИЛИЯ> на свою фамилию, чтобы не конкурировать при выполнении задания с другими студентами.

sudo docker image build -t flask_docker_<ФАМИЛИЯ> .

После успешной сборки образа следующим шагом будет запуск экземпляра образа. Укажите порт, который не занят. Вот как это выполнить:

sudo docker run -p 5600:5000 -d flask_docker_<ФАМИЛИЯ>

Проверить результат:

curl http://127.0.0.1:5600/

Посмотреть логи приложения, который был запущен в контейнере, можно с помощью команды docker logs:

$ sudo docker run -p 5600:5000 -d flask_docker
9b87bb47659eb952c2590052baaa2f026260c84ff1d6739d5dfe3e8fd239713b
~/flaskapp$ curl http://127.0.0.1:5600/
<p>Hello, World!</p>
$ sudo docker logs 9b87bb47659eb952c2590052baaa2f026260c84ff1d6739d5dfe3e8fd239713b
* Serving Flask app 'main'
* Debug mode: on
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.2:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 120-910-507
172.17.0.1 - - [10/Oct/2023 08:09:08] "GET / HTTP/1.1" 200 -

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

Это не все команды, которые позволяют работать с Docker:

docker-command-flow

Подробнее о командах можете узнать из таких статей, как Изучаем Docker, часть 5: команды | Habr или из официальной документации.

Более того, мы не рассматривали вопрос работы с данными в рамках контейнера. Рекомендуется начать со статьи Изучаем Docker, часть 6: работа с данными | Habr.

Для дальнейшего изучения