
В предыдущей статье я встроил команды для работы с веб-камерой в код сервиса робота, а также заменил Wi-Fi антенну на более крупную, чтобы повысить стабильность сигнала. Кроме того, я добавил поддержку управления с клавиатуры, чтобы сделать управление роботом более отзывчивым. В результате мне удалось успешно пройти полосу препятствий, управляя роботом и ориентируясь на изображение с веб-камеры.
В этом материале я создам Docker-контейнер для веб-приложения web-robot-control, который упростит и ускорит его запуск. Также я настрою GitHub Actions для сборки артефакта и его последующей автоматической отправки в Docker Hub.
Статья будет полезна веб-разработчикам, девопсам, которые интересуются созданием Docker-контейнеров и работой с Docker Hub.
Оглавление
Введение
Веб-камера — глаза робота. Пишу веб-приложение на FastAPI для управления DIY-проектом
Инфраструктура и развёртывание проекта
Создание docker контейнера
В данный момент приложение web-robot-control запускается только в режиме разработки с помощью команды poetry run start_app. Кроме того, для запуска требуется ручная настройка виртуального окружения. Очевидно, что простой пользователь не должен заниматься этими шагами самостоятельно. Именно поэтому для упрощения запуска и развёртывания веб-приложений используется контейнеризация с помощью Docker.
Docker — это платформа для упаковки, доставки и запуска приложений в изолированной среде. Docker-контейнер — это лёгкая среда выполнения, содержащая приложение и все его зависимости, благодаря чему приложение работает стабильно и предсказуемо независимо от окружения.
Использование Docker упростит запуск веб-приложения как для обычных пользователей, так и для меня при повседневном использовании. Кроме того, это облегчит распространение моего веб-приложения, так как пользователю будет достаточно загрузить артефакт из Docker Hub и запустить контейнер у себя. Под артефактом я имею в виду готовый Docker-образ, содержащий приложение и все необходимые зависимости для его корректной работы.
Переменные из .env вместо хардкода
Перед написанием конфигурации для Docker и упаковкой веб-приложения в контейнер, я исправлю хардкод параметров (значения, жёстко заданные в коде) host, port и reload в функции start_app(), расположенной в файле start.py. Эта функция использует веб-сервер uvicorn для запуска веб-приложения. Хардкод подобных параметров недопустим для продакшн-версии, поэтому я вынесу host, port и reload в файл .env, а обращаться к ним буду через settings проекта.
Старый код функции start_app():
def start_app():
"""Функция запуска приложения"""
uvicorn.run(
'web_robot_control.main:app',
host='127.0.0.1',
port=8000,
reload=True
)Хардкод в контексте host и port — это жёсткое задание адреса и порта прямо в коде приложения, без возможности изменить их через конфигурацию или переменные окружения. Это снижает гибкость приложения и может приводить к уязвимостям: раскрытие внутренней сетевой структуры, ошибки п��и развёртывании и риск запуска сервиса на небезопасных или конфликтующих портах.
В продакшене reload=True не нужен и вреден: он потребляет лишние ресурсы, запускает дополнительный процесс и может приводить к нестабильному поведению. При разработке этот параметр полезен тем, что автоматически перезапускает сервер при изменении кода.
Добавил в файл .evn.example описание новых переменных.
HOST_APP=ip или доменное имя для app
PORT_APP=порт для app
IS_RELOAD=True для разработки, False для обычного использованияПример новых переменных в .env:
HOST_APP=127.0.0.1
PORT_APP=8070
IS_RELOAD=FalseДалее я добавил новые переменные в класс Settings файла settings.py, к которым буду обращаться из кода веб-приложения.
class Settings(ModelConfig):
"""Класс для данных конфига"""
# Другой код
port_app: int
host_app: str
is_reload: boolЕщё мне нужно добавить папку с тестами tests/ в файл .dockerignore. Это необходимо, чтобы эта папка не попала внутрь контейнера, так как ей там не место.
# app
tests/Пока тестов в этой папке нет, но в будущем я планирую написать статью и код, связанный с тестами. Обычно тесты пишут до сборки контейнера и создания бэкапов артефактов. На данный момент я провёл только ручное тестирование, проверив работоспособность приложения вручную.
Чтобы полностью понимать, как работать с .dockerignore в Python-проекте, можно обратиться к документации и к примеру шаблона для Python-проекта.
Финальным штрихом добавляю settings.host_app, port=settings.port_app и settings.is_reload вместо хардкода в функцию start_app().
import uvicorn
from web_robot_control.settings import settings
def start_app():
"""Функция запуска приложения"""
uvicorn.run(
"web_robot_control.main:app",
host=settings.host_app,
port=settings.port_app,
reload=settings.is_reload,
)Конфигурация для контейнера
Сначала я создал Dockerfile, который выполняет первоначальную настройку контейнера и устанавливает всё необходимое программное обеспечение.
FROM python:3.12.0-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
COPY . /app
RUN pip install poetry
RUN poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansiРазбор Dockerfile:
FROM python:3.12.0-slim— базовый облегчённый образ Python 3.12;WORKDIR /app— рабочая директория внутри контейнера;ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1— отключает буферизацию вывода и создание .pyc файлов, что упрощает работу с логами и ускоряет выполнение кода в контейнере;COPY . /app— копирование всех файлов проекта в контейнер;RUN pip install poetry— установка Poetry для управления зависимостями;RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi— отключает виртуальное окружение и устанавливает зависимости проекта прямо в контейнер, упрощая сборку и запуск приложения.
Для более удобного управления контейнером или даже несколькими контейнерами я использую файл docker-compose.yml.
services:
app:
build: .
image: arduinum628/web-robot-control-app:latest
container_name: web_robot_control
command: poetry run start_app
env_file:
- .env
ports:
- "${PORT_APP}:${PORT_APP}"
volumes:
- app:/app
volumes:
app:Разбор docker-compose.yml:
services:— секция, где описываются все контейнеры (сервисы) проекта;app:— имя сервиса, в данном случае контейнер приложения;build:— указывает, что образ для контейнера нужно собрать из текущей директории, используя Dockerfile;image: arduinum628/web-robot-control-app:latest— задаёт имя образа;container_name: web_robot_control— задаёт имя контейнера для удобства управления;command: poetry run start_app— команда, которая запускается внутри контейнера при старте;env_file:— список файлов с переменными окружения; здесь подключается .env;ports:— проброс портов из контейнера наружу;${PORT_APP}:${PORT_APP}использует переменные окружения;volumes:— подключение томов; app:/app связывает том app с директорией /app внутри контейнера;volumes:(внизу) — определение тома app, который может использоваться для хранения данных вне контейнера.
Теперь всё готово к сборке и последующему запуску контейнера. Для этого уже сейчас можно использовать команды для docker compose. Как его установить для вашей операционной системы, можно узнать в его документации.
Перед тем, как начать сборку контейнера, я решил облегчить себе ввод команд, создав Makefile в корне проекта. Makefile — это файл с правилами автоматизации, который описывает, как собирать, запускать и обслуживать проект (сборка, тесты, деплой), чтобы вместо длинных команд использовать простые make-цели.
Makefile:
help:
@echo "Доступные команды:"
@echo " make docker-build - Собрать Docker образ"
@echo " make docker-rebuild - Пересобрать Docker образ (с очисткой кеша)"
@echo " make start-app-debug - Поднять контейнер с приложением (в режиме debug)"
@echo " make start-app - Поднять контейнер с приложением"
@echo " make stop-app - Остановить контейнер с приложением"
@echo " make docker-pull - Скачать образ web-robot-control-app c Docker Hub"
docker-build:
docker compose build
docker-rebuild:
docker compose build --no-cache
start-app-debug:
docker compose up
start-app:
docker compose up -d
stop-app:
docker stop web_robot_control
docker-pull:
docker pull arduinum628/web-robot-control-appНаписать команду довольно просто:
название команды:
оригинальная команда запускаДостаточно выполнить команду make help, чтобы увидеть список доступных команд, а также их описание.
make helpДоступные команды:
Доступные команды:
make docker-build - Собрать Docker образ
make docker-rebuild - Пересобрать Docker образ (с очисткой кеша)
make start-app-debug - Поднять контейнер с приложением (в режиме debug)
make start-app - Поднять контейнер с приложением
make stop-app - Остановить контейнер с приложением
make docker-pull - Скачать образ web-robot-control-app c Docker HubОписание команд говорит само за себя. Очевидно, для чего предназначена каждая из них. Далее я собираю образ, который будет использоваться для создания контейнера.
make docker-buildРезультат сборки:
✔ Image arduinum628/web-robot-control-app:latest BuiltDocker собрал образ, который можно увидеть, выполнив следующую команду:
docker image lsВывод команды:
IMAGE
arduinum628/web-robot-control-app:latest
ID
131af20f4t6a
DISK USAGE
310MB
CONTENT SIZE
0BРазбор информации об образе:
IMAGE — имя и тег Docker-образа в локальном хранилище (репозиторий
arduinum628/web-robot-control-app, тег latest);ID — уникальный идентификатор образа (используется
Dockerдля ссылок на конкретную версию);DISK USAGE — фактический объём места на диске, который занимает образ (включая слои);
CONTENT SIZE — размер «уникального» содержимого образа без учёта общих слоёв;
0Bозначает, что все слои уже используются другими образами и ничего нового на диск не добавлено.
Теперь данный образ можно использовать для создания контейнера.
Запуск веб-приложения из контейнера
Прежде чем запустить приложение в контейнере, используя образ, собранный выше, я воспользуюсь avahi-daemon. Он позволяет задать имя в сети для моего ПК — так же, как я делал это на Orange Pi в одной из предыдущих статей. Это повышает удобство использования внутри локальной сети и избавляет от необходимости постоянно искать IP-адрес компьютера, который может измениться при следующем запуске ПК или роутера.
На данный момент моё приложение рассчитано на работу внутри домашней сети и не предусматривает использование VPS-серверов и доменов. Я не вижу смысла запускать робота, когда меня нет дома: при потере связи он может куда-то уехать, сломаться и т. д. Я предпочитаю запускать таких роботов только при личном присутствии.
Для начала я открываю файл hosts на ПК, в котором задаются различные имена для IP-адресов внутри системы.
sudo nano /etc/hostsЗадаю имя хоста ПК для его локального IP-адреса. Важно, чтобы это имя совпадало с именем, заданным в avahi-daemon, иначе возникнет конфликт имён, и avahi-daemon не сможет корректно его использовать. С этой проблемой я столкнулся лично, поэтому решил упомянуть её отдельно.
127.0.1.1 mydevice_nameЭто позволит настроить сетевое имя ПК или ноутбука внутри самого устройства, однако другие устройства в локальной сети его не увидят. Далее я перезапускаю службу управления именами хостов, чтобы изменения вступили в силу.
sudo systemctl restart systemd-hostnamedПосле этого мне необходимо установить avahi-daemon на ПК, так как я хочу, чтобы имя хоста было доступно другим устройствам в локальной сети. Это позволит легко подключаться к веб-приложению для управления роботом со смартфона или планшета по имени хоста.
В качестве альтернативы можно установить это веб-приложение на Raspberry Pi, настроить имя хоста и подключаться к нему с любого устройства в локальной сети. Преимущество такого подхода в том, что Raspberry Pi будет выполнять роль локального VPS, а пользователю не придётся каждый раз запускать приложение вручную — достаточно просто открыть URL в браузере.
Устанавливаю avahi-daemon на PC:
sudo apt install avahi-daemonЗадаю имя хоста для ПК в avahi-daemon:
sudo hostnamectl set-hostname mydevice_nameРазбор команды:
hostnamectl— утилита управления именем хоста (systemd);set-hostname— подкоманда для установки нового имени;mydevice— имя хоста, которое сохранится в системе.
Перезапуск службы avahi-daemon для применения настроек:
sudo systemctl restart avahi-daemonПроверяю, что новое имя хоста задано в avahi-daemon.
sudo systemctl status avahi-daemonРезультат выполнения команды:
Server startup complete. Host name is mydevice_name.localИмя хоста соответствует тому, что было задано в avahi-daemon. Теперь можно запускать контейнер с веб-приложением.
make start-appДля проверки я подключаюсь по URL http://mydevice_name.local:8070/ со смартфона.

Интерфейс приложения корректно отобразился на мобильном устройстве. Это означает, что теперь роботом можно управлять со смартфона, лёжа на диване, что заметно повышает мобильность управления. Изображение с камеры отсутствует, так как я не подключался к роботу и не включал его.
Мне очень интересно, как будет ощущаться управление со смартфона, однако перед полноценным использованием я бы хотел добавить стики, как у джойстика, вместо кнопок со стрелками — это должно повысить отзывчивость и удобство управления. Об этом я обязательно напишу в одной из следующих статей.
GitHub как CI: билд Docker-образа в облаке
После проверки сборки Docker-образа на локальном ПК и проверки работы веб-приложения в контейнере можно переходить к настройке CI через GitHub Actions workflows.
Первым шагом я хочу проверить, что контейнер корректно собирается в облаке. Это полезно тем, что в случае ошибки сборки я, как разработчик, сразу узнаю об этом и смогу оперативно исправить код. Событием, при котором будет запускаться сборка контейнера, выбран pull request в ветку master. В дальнейшем, вероятно, я перейду на сборку по тегам версий, но на текущем этапе этого достаточно.
CI (Continuous Integration) — это автоматическая проверка кода при определённых событиях (например, при коммите или создании pull request).
Workflows — это набор автоматизированных шагов в GitHub Actions, которые запускаются при заданных событиях (например, push или pull_request) и используются для выполнения CI/CD-задач.
Для проверки сборки я создаю в корне проекта следующий путь: .github/workflows/build.yml.
В файле build.yml я написал следующую конфигурацию:
name: Build and backup docker conteiner
on:
# Сработает при создании pull request в master ветку
pull_request:
branches:
- master
jobs:
# Сборка контейнера
build:
name: Run build conteiner
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
run: |
make docker-build
env:
PORT_APP: ${{ secrets.PORT_APP }}Разбор конфигурации:
name: Build and backup docker container— имя workflow, которое будет отображаться в интерфейсе GitHub Actions;on:— определяет события, при которых запускается workflow;pull_request:— workflow запускается при создании или обновлении pull request;branches: - master— ограничивает запуск workflow только pull request’ами, направленными в ветку master;jobs:— набор задач, которые будут выполняться в рамках workflow;build:— идентификатор job (внутреннее имя, используется GitHub Actions);name: Run build container— человекочитаемое имя job в интерфейсе GitHub Actions;runs-on: ubuntu-latest— указывает, что job будет выполняться на виртуальной машине с последней доступной версией Ubuntu;steps:— последовательность шагов, выполняемых внутри job;actions/checkout@v4— клонирует репозиторий в рабочую директорию runner’а, без этого дальнейшие шаги не увидят код проекта;docker/setup-buildx-action@v3— настраивает Docker Buildx — современный билд-бэкенд Docker, необходимый для сборки образов (особенно полезен для multi-platform сборок);make docker-build— запускает сборку Docker-образа черезMakefile;env:— передаёт переменные окружения в шаг сборки;PORT_APP: ${{ secrets.PORT_APP }}— значение берётся из GitHub Secrets, в котором безопасно хранятся секретные данные.
Далее необходимо закоммитить изменения и сделать push в репозиторий. Если вы не понимаете, о чём я сейчас написал, то рекомендую ознакомится с документацией git.
После того, как я отправил новые данные в репозиторий на GitHub, я перешёл в свой репозиторий и создал новый pull request: Pull requests → New pull request.
На странице создания pull request GitHub проверяет, можно ли корректно смержить ветки (есть ли конфликты), а также запускает проверки (checks) — то есть workflow’ы GitHub Actions, если они настроены на событие pull_request. В моём случае начался workflow со сборкой Docker-контейнера.

Если какая-либо из jobs в workflow завершается с ошибкой, рядом с pull request появляется красный кружок с крестиком, а также отображается количество проваленных проверок (checks).

Я специально создал ситуацию, при которой проверка не проходит, чтобы показать, что произойдёт в этом случае. В моём случае не была пройдена одна проверка, о чём говорит запись 1 failing check.
Перейдя в интерфейсе Actions → Build and backup docker container → feat: added test_build, можно увидеть стадию job, на которой упала сборка контейнера.

Из лога ошибки видно сообщение: "PORT_APP" variable is not set, что означает, что переменная окружения PORT_APP не задана.
Эта переменная берётся из файла .env, которого нет в репозитории на GitHub. Файл .env обычно не хранят в открытом доступе, так как он содержит конфиденциальные данные, которые должен знать только разработчик приложения.
Однако для GitHub Actions можно задать такие значения через секретные переменные. Для этого нужно перейти в: Settings (репозитория) → Secrets and variables → Actions, а затем нажать кнопку New repository secret, где указать имя переменной и её значение.
Таким образом необходимо добавить все переменные из .env файла, которые используются в процессе выполнения workflow (job’ов).
После этого я снова перешёл к workflow, в котором произошла ошибка, и нажал кнопку Re-run all jobs.

Теперь, судя по тому, что везде отображаются галочки, стадия job Build Docker image успешно пройдена, как и остальные этапы workflow. После этого можно добавлять другие jobs, которые будут выполняться после успешной сборки контейнера.
GitHub как CD: отправка образов на Docker Hub
После проверки сборки контейнера я могу смело отправлять образа (артефакта) на Docker Hub. Docker Hub — это публичный или приватный реестр Docker-образов, предназначенный для их хранения, распространения и версионирования. Хранение артефактов (Docker-образов) в Docker Hub позволяет централизованно использовать готовые сборки в CI/CD, быстро разворачивать приложения и гарантировать воспроизводимость окружения.
Также Docker Hub подходит для распространения вашего ПО по всему миру. Если вас интересуют российские аналоги, можно использовать Yandex Container Registry (Yandex Cloud) или другой отечественный container registry.
Далее мне предстояло написать новую job для отправки образа в Docker Hub в том же workflow-файле. Эта job должна запускаться после успешного выполнения job сборки (build).
# Отправка образа в dockerhub
push_dockerhub:
name: Push image to docker hub
runs-on: ubuntu-latest
needs: build # ждёт успешной сборки
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/web-robot-control-app:latest
build-args: |
PORT_APP=${{ secrets.PORT_APP }}Разбор конфигурации:
push_dockerhub:— имя job в workflow GitHub Actions, отвечающей за публикацию Docker-образа в Docker Hub;name: Push image to docker hub— человекочитаемое название job, отображаемое в интерфейсе GitHub Actions;runs-on: ubuntu-latest— указывает, что job выполняется на виртуальной машине с последней доступной версией Ubuntu;needs: build— задаёт зависимость от jobbuild; данная job начнёт выполняться только после её успешного завершения.
Шаги (steps):
Checkout code— клонирует репозиторий в рабочее окружение runner’а, чтобы Docker мог получить доступ к исходному коду и Dockerfile;Set up Docker Buildx— настраивает Docker Buildx, необходимый для расширенных возможностей сборки (кэширование, multi-platform и push в реестр);Login to Docker Hub— выполняет аутентификацию в Docker Hub с использованием секретов GitHub:
—DOCKERHUB_USERNAME— имя пользователя Docker Hub;
—DOCKERHUB_TOKEN— токен доступа (рекомендуется вместо пароля).Build and push— собирает Docker-образ и отправляет его в Docker Hub:
—context: .— контекст сборки (корень репозитория);
—push: true— указывает, что образ нужно отправить в реестр после сборки;
—tags— имя и тег образа в Docker Hub (latest);
—build-args— передаёт аргументы сборки в Dockerfile, в данном случае значение переменнойPORT_APP.
После написания конфигурации я отправил изменения в репозиторий и перезапустил jobs, в результате чего обе задачи выполнились успешно.

Запуск выполнялся на тестовом закрытом репозитории, поэтому на скриншоте видно имя test_CI_CD. Также здесь отображается артефакт Arduinum~test_CI_CD~SRX5KN.dockerbuild, который представляет собой временный артефакт GitHub Actions, созданный в процессе выполнения workflow для хранения служебных данных сборки Docker-образа (метаданные Buildx, кэш слоёв и результаты сборки). Этот артефакт не является самим Docker-образом, опубликованным в Docker Hub.
На job Push image to docker hub стоит зелёная галочка, значит, она выполнилась успешно, и образ должен быть доступен в Docker Hub. Чтобы это проверить, я перехожу в свой аккаунт Docker Hub, затем во вкладку Repositories, где нахожу репозиторий arduinum628/web-robot-control-app.

Если открыть этот репозиторий, можно увидеть сам Docker-образ и сопутствующую информацию о нём.

Теперь этот образ можно спокойно скачивать и разворачивать у себя, что гораздо удобнее, чем каждый раз собирать его локально с помощью docker compose.
Команда для скачивания образа:
docker pull arduinum628/web-robot-control-app:latestПосле скачивания образа его можно легко использовать для запуска контейнера, выполнив следующие действия (Linux):
Создать
.envв любом удобном месте, используя.env.exampleв качестве шаблона;Скачать образ:
docker pull arduinum628/web-robot-control-app;Создать volume:
docker volume create app;Создать
.envв корне проекта, используя.env.exampleв качестве шаблона;Экспорт
.envпеременных:export $(grep -v '^#' .env | xargs);Запуск приложения:
docker run -d --name web_robot_control -p ${PORT_APP}:${PORT_APP} -v app:/app --env-file .env arduinum628/web-robot-control-app:latest poetry run start_app.
Данный способ удобен тем, что для запуска веб-приложения не требуется клонировать репозиторий с GitHub — достаточно скачать готовый Docker-образ и передать необходимые переменные окружения.
Ссылка на итоговый open-source проект web-robot-control.
Заключение и планы на будущее
Сегодня я создал контейнер для приложения web-robot-control, а также написал две jobs для GitHub Actions. Результат — успешная сборка контейнера и загрузка образа на Docker Hub.
Следующая статья (связанная с данным проектом) будет посвящена продолжению темы CI/CD — облегчению запуска и сборки приложения, а также его доставки. На этот раз я соберу в GitHub Actions пакет для Linux и организую его хранение и распространение через внешнее хранилище. Также я напишу инструкцию для запуска пользователю в README.md, скачаю пакет по этой инструкции и проверю его работу на одноплатном компьютере.
Если у вас есть идеи по развитию проекта, поделитесь ими в комментариях — буду рад услышать ваши предложения!
Автор статьи @Arduinum
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.
