Введение
В этом лабе мы будем основываться на знаниях из лабы 1, где мы использовали команды Docker для запуска контейнеров. Мы создадим собственный Docker-образ, построенный на основе Dockerfile. После того, как мы построим образ, мы отправим его в центральный реестр, откуда его можно будет извлечь для развертывания в других средах. Также мы кратко опишем слои образов и то, как Docker интегрирует "копирование при записи" и объединённую файловую систему для эффективного хранения образов и запуска контейнеров.
В этом лабе мы будем использовать несколько команд Docker. Для полной документации по доступным командам проверьте официальную документацию.
Создать приложение на Python (без использования Docker)
Выполните следующую команду, чтобы создать файл с именем app.py с простой программой на Python. (скопируйте и вставьте весь код блока)
cd ~/project
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
Это простое приложение на Python, которое использует Flask для предоставления HTTP-сервера на порту 5000 (5000 - это стандартный порт для Flask). Не беспокойтесь, если вы не слишком знакомы с Python или Flask, эти концепции могут быть применены к приложению, написанному на любом языке.
По желанию: Если у вас установлены Python и pip, вы можете запустить это приложение локально. Если нет, перейдите к следующему шагу.
$ python3 --version
$ pip3 --version
$ pip3 install flask
$ python3 app.py
* 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
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Откройте приложение в новой вкладке браузера, используя http://0.0.0.0:5000/.

Создать и собрать Docker-образ
А что, если у вас не установлена Python локально? Не беспокойтесь! Потому что вам это не нужно. Одним из преимуществ использования контейнеров является то, что вы можете устанавливать Python внутри контейнеров, не устанавливая его на хост-машине.
Создайте Dockerfile, выполнив следующую команду. (скопируйте и вставьте весь код блока)
echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile
Dockerfile перечисляет инструкции, необходимые для сборки Docker-образа. Давайте рассмотрим этот файл построчно.
FROM python:3.8-alpine
Это точка начала для вашего Dockerfile. Каждый Dockerfile должен начинаться с строки FROM, которая является базовым образом, на котором вы будете строить свои слои.
В этом случае мы выбираем базовый слой python:3.8-alpine (см. Dockerfile для python3.8/alpine3.12), так как в нем уже есть нужная версия Python и pip для запуска нашего приложения.
Версия alpine означает, что она использует дистрибутив Alpine Linux, который значительно меньше многих альтернативных дистрибутивов Linux, размером около 8 МБ, в то время как минимальная установка на диск может быть около 130 МБ. Менее большой образ означает, что он будет скачиваться (разворачиваться) намного быстрее, и он также имеет преимущества для безопасности, так как имеет меньшую поверхность атаки. Alpine Linux - это дистрибутив Linux, основанный на musl и BusyBox.
Здесь мы используем тег "3.8-alpine" для образа Python. Посмотрите на доступные теги для официального образа Python на Docker Hub. Лучшим практикой является использование конкретного тега при наследовании родительского образа, чтобы контролировать изменения в родительской зависимости. Если тег не указан, то действует тег "latest", который является динамической ссылкой, указывающей на последнюю версию образа.
出于安全考虑,了解您在其上构建 Docker 镜像的层非常重要。因此,强烈建议仅使用在 docker hub 中找到的“官方”镜像,或在 docker-store 中找到的非社区镜像。这些镜像经过 审核 以满足某些安全要求,并且也有非常好的文档供用户参考。您可以在 docker hub 上找到有关此 Python 基础镜像 以及您可以使用的所有其他镜像的更多信息。
对于更复杂的应用程序,您可能会发现需要使用更高层次的 FROM 镜像。例如,我们的 Python 应用程序的父 Dockerfile 以 FROM alpine 开头,然后为镜像指定一系列 CMD 和 RUN 命令。如果您需要更细粒度的控制,可以从 FROM alpine(或其他发行版)开始并自己运行这些步骤。不过,首先我建议使用与您的需求密切匹配的官方镜像。
RUN pip install flask
RUN команда выполняет команды, необходимые для настройки образа для вашего приложения, таких как установка пакетов, редактирование файлов или изменение прав доступа к файлам. В этом случае мы устанавливаем flask. Команды RUN выполняются во время сборки и добавляются в слои вашего образа.
CMD ["python","app.py"]
CMD - это команда, которая выполняется при запуске контейнера. Здесь мы используем CMD для запуска нашего приложения на Python.
В каждом Dockerfile может быть только одна команда CMD. Если вы укажите несколько команд CMD, то последняя команда CMD будет действовать. Родительский образ python:3.8-alpine также задает CMD (CMD python3). Вы можете найти Dockerfile для официального образа python:alpine здесь.
Вы можете напрямую использовать официальный образ Python для запуска скриптов на Python, не устанавливая Python на вашем хосте. Но сегодня мы создаем собственный образ, чтобы включить наш исходный код, чтобы мы могли собрать образ с нашим приложением и разместить его в других средах.
COPY app.py /app.py
Это копирует app.py в локальной директории (где вы будете запускать docker image build) в новый слой образа. Эта инструкция - последняя строка в Dockerfile. Слои, которые часто изменяются, такие как копирование исходного кода в образ, должны быть расположены ближе к концу файла, чтобы充分利用 Docker 层缓存。这使我们能够避免重建原本可以缓存的层。例如,如果 FROM 指令发生更改,它将使此镜像的所有后续层的缓存无效。我们将在本实验后面演示这一点。
将此放在 CMD ["python","app.py"] 行之后似乎违反直觉。请记住,CMD 行仅在容器启动时执行,因此我们在此处不会收到“文件未找到”错误。
Вот и все: очень простой Dockerfile. Полный список команд, которые вы можете поместить в Dockerfile, можно найти здесь. Теперь, когда мы определили наш Dockerfile, давайте используем его для сборки нашего собственного Docker-образа.
Соберите Docker-образ.
Передайте -t, чтобы присвоить имени вашему образу python-hello-world.
docker image build -t python-hello-world.
Проверьте, что ваш образ появился в списке ваших образов.
docker image ls
Обратите внимание, что ваш базовый образ python:3.8-alpine также находится в списке.
Вы можете выполнить команду history, чтобы показать историю образа и его слоев,
docker history python-hello-world
docker history python:3.8-alpine
Запустить Docker-образ
Теперь, когда вы собрали образ, вы можете запустить его, чтобы убедиться, что он работает.
Запустите Docker-образ
docker run -p 5001:5000 -d python-hello-world
Флаг -p отображает порт, работающий внутри контейнера, на ваш хост. В этом случае мы отображаем приложение на Python, работающее на порту 5000 внутри контейнера, на порт 5001 на вашем хосте. Обратите внимание, что если порт 5001 уже занят другой программой на вашем хосте, вы можете придется заменить 5001 на другое значение, например, 5002.
Перейдите на вкладку PORTS в окне терминала и нажмите на ссылку, чтобы открыть приложение в новой вкладке браузера.

В терминале выполните curl localhost:5001, который возвращает hello world!.
Проверьте вывод журнала контейнера.
Если вы хотите увидеть логи из вашего приложения, вы можете использовать команду docker container logs. По умолчанию docker container logs выводит то, что отправляется в стандартный вывод вашим приложением. Используйте docker container ls, чтобы найти идентификатор для вашего запущенного контейнера.
labex:project/ $ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52df977e5541 python-hello-world "python app.py" 2 minutes ago Up 2 minutes 0.0.0.0:5001->5000/tcp, :::5001->5000/tcp heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
* Serving Flask app 'app'
* 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.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -
Dockerfile - это то, как вы создаете воспроизводимые сборки для вашего приложения. Общий рабочий процесс заключается в том, чтобы ваша автоматизация CI/CD выполняла docker image build в качестве части своего процесса сборки. Как только образовы будут собраны, они будут отправлены в центральный реестр, откуда их могут получить все среда (например, тестовая среда), которые нуждаются в запуске экземпляров этого приложения. В следующем шаге мы отправим наш собственный образ в публичный Docker-реестр: Docker Hub, где его могут использовать другие разработчики и операторы.
Отправить в центральный реестр
Перейдите на Docker Hub и создайте учетную запись, если вы еще не сделали этого. Альтернативно вы также можете использовать, например, https://quay.io.
Для этой лабораторной работы мы будем использовать Docker Hub в качестве центрального реестра. Docker Hub - это бесплатная служба для хранения публичных образов, или вы можете платить за хранение приватных образов. Перейдите на сайт Docker Hub и создайте бесплатную учетную запись.
Большинство организаций, которые активно используют Docker, создают собственный реестр внутри своей инфраструктуры. Чтобы упростить процесс, мы будем использовать Docker Hub, но следующие концепции применимые ко всем регистрам.
Войти
Вы можете войти в учетную запись реестра образов, введя docker login в терминале, или если вы используете podman, введите podman login.
labex:project/ $ export DOCKERHUB_USERNAME=<ваше_имя_пользователя_Docker>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Пароль:
ВНИМАНИЕ! Ваш пароль будет храниться в незашифрованном виде в /home/labex/.docker/config.json.
Настройте помощника учетных данных, чтобы убрать это предупреждение. См.
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Вход выполнен успешно
Пометьте ваш образ именем пользователя
Соглашение о именовании Docker Hub заключается в том, чтобы пометить ваш образ именем [имя_пользователя_dockerhub]/[имя_образа]. Для этого мы будем помечать ранее созданный образ python-hello-world чтобы соответствовать этому формату.
docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world
Отправьте ваш образ в реестр
После того, как у нас есть правильно помеченный образ, мы можем использовать команду docker push, чтобы отправить наш образ в реестр Docker Hub.
docker push $DOCKERHUB_USERNAME/python-hello-world
Посмотрите на ваш образ в Docker Hub в браузере
Перейдите на Docker Hub и перейдите в свой профиль, чтобы увидеть ваш недавно загруженный образ по адресу https://hub.docker.com/repository/docker/<имя_пользователя_dockerhub>/python-hello-world.
Теперь, когда ваш образ находится в Docker Hub, другие разработчики и операторы могут использовать команду docker pull, чтобы развернуть ваш образ в других средах.
Примечание: Docker-образы содержат все зависимости, необходимые для запуска приложения внутри образа. Это полезно, потому что мы больше не имеем проблем с отклонением окружения (различиями в версиях), когда мы rely на зависимости, которые устанавливаются на каждой среде, на которую мы разворачиваем наше приложение. Мы также не должны выполнять дополнительные шаги по подготовке этих сред. Просто один шаг: установите Docker, и вы готовы к работе.
Развертывание изменений
Приложение "Hello World!" переоценировано. Давайте обновим приложение, чтобы оно вместо этого говорило "Hello Beautiful World!".
Обновить app.py
Замените строку "Hello World" на "Hello Beautiful World!" в app.py. Вы можете обновить файл с помощью следующей команды. (скопируйте и вставьте весь код блока)
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello beautiful world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
Пересобрать и отправить ваш образ
Теперь, когда ваше приложение обновлено, вам нужно повторить шаги выше, чтобы пересобрать ваше приложение и отправить его в реестр Docker Hub.
Сначала пересоберите, на этот раз используйте имя пользователя Docker Hub в команде сборки:
docker image build -t $DOCKERHUB_USERNAME/python-hello-world.
Обратите внимание на "Using cache" для шагов 1-3. Эти слои Docker-образа уже были собраны, и docker image build будет использовать эти слои из кеша вместо их пересборки.
docker push $DOCKERHUB_USERNAME/python-hello-world
Также есть механизм кеширования для отправки слоев. Docker Hub уже имеет все, кроме одного слоя, от предыдущей отправки, поэтому он отправляет только тот слой, который изменился.
Когда вы изменяете слой, каждый слой, построенный поверх него, будет необходимо пересобрать. Каждая строка в Dockerfile создает новый слой, который строится поверх слоя, созданного из предыдущих строк. Именно поэтому порядок строк в нашем Dockerfile важен. Мы оптимизировали наш Dockerfile так, чтобы слой, наиболее вероятный измениться (COPY app.py /app.py), был последней строкой Dockerfile. Как правило, для приложения изменения в коде происходят наиболее часто. Эта оптимизация особенно важна для процессов CI/CD, где вы хотите, чтобы ваша автоматизация работала как можно быстрее.
Понимание слоев образов
Одной из основных особенностей дизайна Docker является использование объединенной файловой системы.
Рассмотрим Dockerfile, который мы создали ранее:
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py
Каждой из этих строк соответствует слой. Каждый слой содержит только дельту, разницу или изменения по сравнению с предыдущими слоями. Чтобы собрать эти слои в единый работающий контейнер, Docker использует объединенную файловую систему, чтобы наложить слои прозрачно в единый вид.
Каждый слой образа является только для чтения, за исключением самого верхнего слоя, который создается для работающего контейнера. Читательско-записной слой контейнера реализует "копирование при записи", что означает, что файлы, хранящиеся в нижних слоях образа, поднимаются в читательско-записной слой контейнера только при внесении изменений в эти файлы. Затем эти изменения сохраняются в слое работающего контейнера. Функция "копирование при записи" очень быстрая и в подавляющем большинстве случаев не оказывает заметного влияния на производительность. Вы можете проверить, какие файлы были подняты на уровень контейнера, с помощью команды docker diff. Более подробную информацию о том, как использовать docker diff, можно найти здесь.

Поскольку слои образов только для чтения, они могут быть разделены между образами и работающими контейнерами. Например, создание нового приложения на Python с собственным Dockerfile с похожими базовыми слоями, позволит разделить все общие слои с первым приложением на Python.
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py

Вы также можете убедиться в разделении слоев, когда запускаете несколько контейнеров из одного образа. Поскольку контейнеры используют одинаковые слои только для чтения, вы можете представить, что запуск контейнеров происходит очень быстро и занимает очень мало места на хост-машине.
Вы, возможно, заметите, что в этом Dockerfile и в Dockerfile, который вы создали ранее в этой лабораторной работе, есть повторяющиеся строки. Хотя это очень простой пример, вы можете выделить общие строки обоих Dockerfile в "базовый" Dockerfile, а затем ссылаться на него из каждого из ваших дочерних Dockerfile с помощью команды FROM.
Слоирование образов позволяет использовать механизм кеширования Docker для сборок и отправок. Например, вывод вашей последней команды docker push показывает, что некоторые слои вашего образа уже существуют в Docker Hub.
$ docker push $DOCKERHUB_USERNAME/python-hello-world
Для более тщательного изучения слоев вы можете использовать команду docker image history для образа Python, который мы создали.
$ docker image history python-hello-world
Каждая строка представляет собой слой образа. Вы заметите, что верхние строки соответствуют вашему созданному Dockerfile, а строки ниже извлекаются из родительского образа Python. Не беспокойтесь о тегах "<missing>". Это все еще нормальные слои; они просто не получили идентификатор от системы Docker.
Очистка
Завершение этой лабораторной работы приводит к появлению большого количества работающих контейнеров на вашем хосте. Давайте их очистим.
Запустите docker container stop [идентификатор_контейнера] для каждого работающего контейнера
Сначала получите список работающих контейнеров с помощью docker container ls.
$ docker container ls
Затем выполните команду для каждого контейнера в списке.
$ docker container stop <идентификатор_контейнера>
Удалите остановленные контейнеры
docker system prune - это очень удобная команда для очистки системы. Она удалит все остановленные контейнеры, неиспользуемые тома и сети, а также "висячие" (dangling) образы.
$ docker system prune
ВНИМАНИЕ! Это удалит:
- все остановленные контейнеры
- все тома, не используемые ни одним контейнером
- все сети, не используемые ни одним контейнером
- все "висячие" образы
Вы уверены, что хотите продолжить? [y/N] y
Удаленные контейнеры:
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26
Всего освобождено места: 300.3kB
Резюме
В этой лабораторной работе вы начали получать пользу, создавая собственные пользовательские Docker-контейнеры.
Основные выводы:
- Dockerfile - это то, как вы создаете воспроизводимые сборки для вашего приложения и как интегрируете ваше приложение с Docker в конвейер CI/CD.
- Docker-образы могут быть доступны для всех ваших сред через центральный реестр. Docker Hub - это один пример реестра, но вы можете развернуть собственный реестр на серверах, которые вы контролируете.
- Docker-образы содержат все зависимости, необходимые для запуска приложения внутри образа. Это полезно, потому что мы больше не имеем проблем с отклонением окружения (различиями в версиях), когда мы rely на зависимости, которые устанавливаются на каждой среде, на которую мы разворачиваем наше приложение.
- Docker использует объединенную файловую систему и "копирование при записи", чтобы повторно использовать слои образов. Это уменьшает размер хранения образов и значительно повышает производительность запуска контейнеров.
- Слои образов кешируются системой сборки и отправки Docker. Не нужно пересобирать или повторно отправлять слои образов, которые уже присутствуют на целевой системе.
- Каждая строка в Dockerfile создает новый слой, и из-за кеширования слоев строки, которые изменяются чаще (например, добавление исходного кода в образ), должны быть перечислены ближе к концу файла.