Перейти к контенту

Применение Portainer в CI/CD процессах

DevOps Иван Ашихмин 789

Третья статья про использование Portainer. В этой статье узнаем как написать CI/CD для сборки Docker-образа и деплоя проекта, а также, как получить бесплатно Portainer BE.

Применение Portainer в CI/CD процессах
DevOps Иван Ашихмин 789

Продолжаем знакомиться с Portainer и сферами его применения.

В двух прошлых статьях:

Если вы здесь впервые: Portainer — это веб-панель, которая упрощает работу с Docker (и Docker Swarm/Kubernetes): запуск контейнеров, обновления, сети, тома и права доступа — всё в одном интерфейсе.

Мы уже разобрались:

  • что такое Portainer;
  • что за сервис DockerHosting.ru и как он помогает быстро стартовать;
  • как писать Dockerfile и docker-compose.yml;
  • как развернуть проект из Git-репозитория;
  • как подключать к Portainer удалённые серверы и управлять на них контейнерами Docker.

В этой статье разберём, как встроить Portainer в процессы CI/CD и где хранить Docker-образы, а также узнаем, как бесплатно получить и активировать Portainer Business Edition.

Коротко для тех, кто только знакомится с темой: 
CI (Continuous Integration) — автоматическая сборка и проверка кода при каждом изменении. 
CD (Continuous Delivery/Deployment) — автоматическая доставка/развёртывание собранного приложения. 
Регистр образов (registry) — хранилище Docker-образов (публичное или приватное), из которого Portainer и ваши сервера забирают готовые образы для деплоя.

Также рекомендую к прочтению «CI/CD: основы написания Workflow», чтобы глубже понять понятия CI/CD и Workflow.


 

Где хранить Docker-образы

Начнём с хранилищ (Docker Registry). Dockerfile описывает, как собрать образ, а запись build: . в docker-compose.yml собирает его локально.

Однако в реальных проектах удобнее другой способ: собрать образ один раз и отправить его в реестр. Тогда при деплое сервер просто выполнит docker pull и заберёт готовый образ. Такой подход идеально подходит для CI/CD: вся тяжёлая сборка выполняется в конвейере, а на продакшене остаётся лишь запуск контейнера.

 

Какие бывают реестры

  • Docker Hub — самое популярное публичное хранилище.
  • Self-hosted решения, например Harbor — реестр, который вы разворачиваете на своём сервере.
  • Реестры у git-хостингов: GitHub Container Registry (ghcr.io), GitLab Container Registry, встроенные решения в Gitea и другие.

 

Чем они отличаются

  • Docker Hub
    • Плюсы: легко начать, много готовых публичных образов.
    • Минусы: ограничения на бесплатных планах (скорость и объём хранения), зависимость от внешнего сервиса.
  • Self-hosted (например, Harbor)
    • Плюсы: полный контроль над хранилищем, приватность, собственные политики безопасности, возможность репликации.
    • Минусы: нужно администрирование, дополнительные ресурсы и поддержка.
  • Git-хостинги (GHCR/GitLab и др.)
    • Плюсы: образы хранятся рядом с кодом, удобно настраивать доступ, хорошая интеграция с CI/CD.
    • Минусы: привязка к конкретной платформе и её лимитам.

 

Как это работает

  1. Собираете образ из Dockerfile — локально или в CI/CD.
  2. Присваиваете тег:
    • фиксированный (например, версия 1.2.3 или SHA коммита),
    • плавающий (latest). 
      Хорошая практика — использовать оба тега: фиксированный для надёжности и latest для быстрого запуска.
  3. Отправляете образ в реестр (docker push работает примерно как git push, только вместо кода загружается образ).
  4. На сервере запускаете docker-compose.yml и вместо build: . указываете image:
    Для продакшена безопаснее использовать конкретный тег или дайджест (@sha256:…), а не только latest.

Я, например, использую свой git-сервер на базе Gitea для хранения и кода, и Docker-образов. Но в этой статье мы будем работать с реестром от GitHub — GHCR (ghcr.io)
Если хотите подробнее про self-hosted-хранилища — напишите в комментариях, и я подготовлю отдельный материал.


 

Проект для демонстрации

Для примера возьмём небольшой проект с GitHub: https://github.com/proDreams/tempProject.

Это Telegram-бот, который отвечает на любое сообщение текстом «Hello World!»
Он нам нужен, чтобы показать все шаги на реальном проекте, а вы потом сможете легко адаптировать их под свой.

Разберём, что уже есть в репозитории.

 

Dockerfile

Dockerfile выглядит так:

FROM python:3.13-slim  

WORKDIR /code  

COPY requirements.txt /code  

RUN pip install --upgrade pip && pip install -r requirements.txt  

COPY . /code  

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

Что здесь происходит шаг за шагом:

  1. FROM python:3.13-slim 
    Используем официальный базовый образ с Python 3.13 на облегчённой системе (slim). Такой образ весит меньше и быстрее скачивается.
  2. WORKDIR /code 
    Устанавливаем рабочую директорию внутри контейнера. Все команды ниже будут выполняться именно в этой папке.
  3. COPY requirements.txt /code 
    Копируем список зависимостей. Это сделано отдельным шагом, чтобы Docker мог закешировать установку пакетов и не ставил их заново при каждой пересборке.
  4. RUN pip install --upgrade pip && pip install -r requirements.txt 
    Обновляем pip и устанавливаем зависимости из requirements.txt
    Благодаря предыдущему шагу, если requirements.txt не изменился, этот этап будет пропущен (Docker возьмёт данные из кеша).
  5. COPY . /code 
    Копируем остальной код проекта в контейнер.
  6. CMD ["python", "./main.py"] 
    Задаём команду, которая будет выполняться при запуске контейнера — запуск нашего Telegram-бота. 
    Важно: CMD можно переопределить при запуске (docker run ...), а вот ENTRYPOINT заменить сложнее.

Этот Dockerfile уже подходит для нашего примера, менять его не нужно.

 

docker-compose.yaml

Сейчас файл docker-compose.yaml выглядит так:

services:  
  test-bot:  
    build: .  
    container_name: test-bot  
    environment:  
      - BOT_TOKEN=${BOT_TOKEN}

Разберём, что здесь происходит:

  • Создаётся сервис test-bot, для которого Docker собирает образ из текущей папки (build: .) по Dockerfile.
  • Контейнер получает понятное имя test-bot. Это облегчает поиск в списке контейнеров, просмотр логов и выполнение команд.
  • В контейнер передаётся переменная окружения BOT_TOKEN. Её значение берётся:
    • либо из переменной окружения на хосте,
    • либо из файла .env, если он находится рядом с docker-compose.yaml.

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

Позже, когда мы подключим CI/CD, строчку build: . заменим на image: ..., чтобы использовать заранее собранный и загруженный в реестр образ.

 

main.py

Код бота:

import asyncio  
import os  

from aiogram import Dispatcher, Bot  
from aiogram.types import Message  
from dotenv import load_dotenv  

load_dotenv()  

async def send_message(message: Message) -> None:  
    await message.answer(text="Hello World!")  

async def start() -> None:  
    bot = Bot(token=os.getenv('BOT_TOKEN'))  
    dp = Dispatcher()  

    dp.message.register(send_message)  

    try:  
        await dp.start_polling(bot)  
    finally:  
        await bot.session.close()  


if __name__  "__main__":  
    asyncio.run(start())

Что здесь происходит:

  • load_dotenv() 
    Загружает переменные окружения из файла .env
    Это нужно только при запуске локально. В Docker Compose переменные подтягиваются автоматически из секции environment или через env_file
    То есть в контейнере этот вызов необязателен, но и не мешает.
  • Обработчик сообщений 
    Функция send_message отвечает на любое входящее сообщение фразой «Hello World!».
  • Функция start()
    • создаёт объект Bot с токеном из переменной BOT_TOKEN;
    • инициализирует Dispatcher;
    • регистрирует обработчик для всех сообщений (dp.message.register(send_message)).
  • Запуск long polling 
    dp.start_polling(bot) включает механизм long polling — бот регулярно опрашивает серверы Telegram на наличие новых сообщений.
  • Корректное завершение работы 
    В блоке finally закрывается HTTP-сессия бота, чтобы не оставалось «висячих» соединений.
  • Запуск из файла 
    if __name__ "__main__": asyncio.run(start()) гарантирует, что бот запустится только если вы запускаете этот файл напрямую, а не импортируете его в другой модуль.

 

Workflow сборки образа

Теперь перейдём к практике. Наша цель — настроить Workflow, который будет собирать Docker-образ проекта и отправлять его в реестр GitHub.

Для этого воспользуемся GitHub Actions — встроенным в GitHub сервисом для CI/CD. Он позволяет автоматически запускать сценарии при каждом пуше в репозиторий (или по другим событиям).

Структура будет такой:

  • В корне проекта создаём директорию .github — здесь хранится всё, что связано с настройками GitHub.
  • Внутри неё создаём папку workflows — именно здесь GitHub Actions ищет сценарии для запуска.
  • В этой папке создаём файл build_and_deploy.yaml
    В нём мы опишем задачи по сборке и деплою проекта. Для начала сделаем только сборку.

 

build_and_deploy.yaml

Файл Workflow целиком:

name: Build and Deploy Project  

on:  
  push:  
    branches:  
      - main  

permissions:  
  packages: write  
  contents: read  

jobs:  
  build-and-push:  
    runs-on: ubuntu-latest  

    steps:  
      - name: Checkout repository  
        uses: actions/checkout@v4  

      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v3  

      - name: Log in to GitHub Container Registry  
        uses: docker/login-action@v3  
        with:  
          registry: ghcr.io  
          username: ${{ github.actor }}  
          password: ${{ secrets.GITHUB_TOKEN }}  

      - name: Build and push Docker image  
        uses: docker/build-push-action@v6  
        with:  
          context: .  
          push: true  
          cache-from: type=registry,ref=ghcr.io/prodreams/tempproject:latest  
          cache-to: type=inline  
          tags: |  
            ghcr.io/prodreams/tempproject:latest    
            ghcr.io/prodreams/tempproject:${{ github.sha }}

Что здесь происходит

  • name 
    Просто название Workflow. Оно отображается во вкладке Actions на GitHub.
  • on.push.branches: main 
    Запускаем Workflow при каждом пуше в ветку main.
    Если вы работаете в другой ветке для продакшена, замените main на нужную (например, production).
  • permissions Даём встроенному GITHUB_TOKEN права:
    • packages: write — загружать образы в GitHub Container Registry,
    • contents: read — читать код из репозитория.
  • runs-on: ubuntu-latest 
    GitHub запускает сборку на Linux-раннере (виртуальной машине Ubuntu).
  • Checkout 
    Забираем код вашего репозитория на раннер. Без этого Docker не увидит Dockerfile и исходники.
  • Buildx 
    Подключаем расширенный механизм сборки Docker. 
    Он поддерживает кеширование и мультиархитектуру (например, если вы захотите собирать образы под ARM).
  • Login в GHCR 
    Авторизуемся в GitHub Container Registry (ghcr.io) с помощью встроенного GITHUB_TOKEN.
    Если при пуше в организацию получите ошибку 403, проверьте:
    1. Включено ли использование GitHub Actions для пакетов в настройках организации.
    2. Нужен ли вам персональный токен (PAT) с правами write:packages.
  • Build and push Docker image
    • context: . — собираем образ из текущей директории.
    • push: true — отправляем образ в реестр.
    • cache-from / cache-to — включаем кеш, чтобы повторные сборки были быстрее. 
      (Если вы запускаете Workflow впервые, кеша ещё не будет — это нормально).
    • tags — публикуем образ сразу с двумя тегами:
      • latest — всегда указывает на последнюю версию;
      • ${{ github.sha }} — уникальный тег по коммиту (нужен, чтобы откатиться к точной версии).

 

Откуда взять адрес?

После выполнения Workflow образ публикуется в GHCR с тегами latest и уникальным хэшем коммита (${{ github.sha }}). 
Чтобы использовать этот образ в docker-compose.yml, нужно указать полный адрес вместе с тегом.

Шаблон для GHCR:

ghcr.io/<организация_или_имя_пользователя>/<название_образа>:<тег>

Как составить адрес (даже если образ ещё не опубликован):

  • ghcr.io/ — домен GitHub Container Registry.
  • <имя_пользователя> или <организация> — ваш логин GitHub или название организации (строго в нижнем регистре).
  • <название_образа> — обычно совпадает с именем репозитория (тоже в нижнем регистре).
  • :тег — версия образа. Это может быть latest, 1.0.0, короткий SHA коммита или ${{ github.sha }}.

⚠️ Важно: имя образа (всё до двоеточия с тегом) должно быть в нижнем регистре, иначе Docker выдаст ошибку invalid reference format.

Для репозитория https://github.com/proDreams/tempProject корректный префикс будет:

ghcr.io/prodreams/tempproject

Примеры полных адресов:

ghcr.io/prodreams/tempproject:latest
ghcr.io/prodreams/tempproject:${{ github.sha }}

 

Отправляем изменения в GitHub и запускаем Workflow

Файл Workflow готов — пора отправить его в репозиторий. 
Когда коммит попадёт на GitHub и сработает триггер (у нас — пуш в ветку main), Actions автоматически запустит сборку.

Выполняем команды в терминале:

git add .github/workflows/build_and_deploy.yaml

git commit -m "CI/CD: Build docker image"

git push

После этого заходим на страницу репозитория и открываем вкладку Actions. Там отобразится выполняющийся Workflow:

 

Кликаем по запуску, чтобы посмотреть список заданий (jobs). В нашем случае оно одно и на него можно кликнуть:

 

Откроется подробный лог выполнения. Здесь можно увидеть каждый шаг и, если что-то пойдёт не так, понять причину:

 

У нас всё прошло успешно — образ отправился в реестр. 
Теперь на главной странице репозитория в правом блоке Packages появится наш Docker-образ:

 

Если перейти по пакету, откроется страница с подробной информацией об образе:

 

Если что-то не сработало

  • Workflow не стартовал
    • проверьте, что пушите именно в ветку main (или измените ветку в триггере),
    • убедитесь, что файл лежит в .github/workflows/...,
    • проверьте, включены ли Actions в настройках репозитория.
  • Ошибка при пуше в GHCR
    • убедитесь, что в Workflow указано permissions: packages: write,
    • при работе с организацией проверьте, что публикация пакетов из Actions разрешена,
    • если пакет приватный — настройте права доступа.

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

  • latest — всегда на последнюю сборку;
  • ${{ github.sha }} — конкретная версия для стабильного деплоя.

 

Получаем и активируем Portainer Business Edition

Может возникнуть вопрос: зачем переходить на Business Edition, если до этого хватало версии Community
Дело в том, что CE (Community Edition) покрывает базовые потребности: можно управлять Docker-хостами, контейнерами и стаками. 
Однако, например, создание Webhook’ов для автоматического обновления стаков и контейнеров там недоступно — это уже функционал Business Edition.

Хорошая новость: Business Edition можно получить бесплатно, но с одним ограничением — не более трёх Docker-хостов (включая локальный). 
Для домашних или небольших проектов этого обычно достаточно.

 

Как получить ключ

Переходим на страницу программы Take 3
👉 https://www.portainer.io/take-3

 

Справа находится форма. Заполняем её:

  • Имя и фамилия
  • Электронная почта — укажите рабочую, именно туда придёт ключ.
  • Номер телефона — российский номер проходит без проблем.
  • Страна — выбираем «Россия» (есть в списке).
  • Использовали ли вы Portainer CE? — отвечаем «Да».
  • Используемая платформа контейнеризации — в моём случае «Docker Standalone».
  • Как вы используете Portainer? — я указал «Для дома».
  • Как узнали о Portainer — выбирайте любой подходящий вариант.
  • И не забудьте поставить галочку согласия с лицензионным соглашением.

После нажатия «Submit» ждём письмо на почту с кодом активации.

 

Обновление Portainer CE до BE

Обновить Portainer до Business Edition очень просто.

Если вы используете DockerHosting.ru, то предустановленный Portainer доступен по адресу:

https://<ip_сервера>:9000/

В верхнем левом углу, над логотипом, есть кнопка «Upgrade to Business Edition». Нажимаем её — откроется окно для ввода ключа:

 

Вводим полученный ключ и нажимаем «Start upgrade».

После этого начнётся процесс обновления. Когда он завершится, система попросит вас снова войти в аккаунт:

 

⚡ Важно: при обновлении до BE ваши контейнеры, стеки и настройки не пропадут
Portainer просто активирует дополнительные функции, в том числе возможность использовать Webhook’и.


 

Подключение реестра в Portainer

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

В левом меню выберите Registries — откроется список подключённых реестров:

 

Нажмите Add Registry. Вы попадёте на страницу добавления:

 

Из вариантов выбираем GitHub (для GitHub Container Registry):

 

Теперь заполняем поля:

  • Name — любое название, например github.
  • Username — ваш логин GitHub.
  • Personal Access Token — ваш персональный токен GitHub. 
    Создать его можно здесь: 👉 https://github.com/settings/tokens

Для скачивания образов достаточно права read:packages
Не давайте лишних разрешений: Portainer будет только читать образы, а пушит их ваш CI.

После сохранения вы вернётесь к списку реестров, где появится подключение к GitHub:

 

Примечания

  • Если образы находятся в приватной организации, убедитесь, что токен имеет доступ к этой организации и к пакетам.
  • Для Portainer используйте минимально достаточные права (обычно read-only).
  • ⚠️ Не путайте Personal Access Token с GITHUB_TOKEN, который используется в GitHub Actions. 
    GITHUB_TOKEN работает только внутри CI, а для Portainer нужен отдельный PAT.

 

Добавление GitHub Container Registry в Portainer CE

В Portainer Community Edition нет отдельного готового варианта для GitHub, но подключить реестр всё равно можно без проблем. 
Для этого при добавлении выбираем Custom registry:

 

Далее заполняем поля:

  • Name — любое удобное название, например GitHub.
  • Registry URL — указываем ghcr.io.
  • Включаем Authentication и вводим:
    • Username — ваш логин GitHub,
    • Password/Token — персональный токен (PAT). Создать его можно здесь: 👉 https://github.com/settings/tokens.

Для скачивания образов достаточно права read:packages
Помните: этот токен отличается от GITHUB_TOKEN, который используется внутри GitHub Actions. Здесь нужен именно ваш PAT.

После нажатия Add registry реестр появится в списке и будет готов к использованию.


 

Создание стека в Portainer

В первой статье мы уже пробовали создавать стек из Git-репозитория
Теперь сделаем то же самое, но используем только файл docker-compose.yml — немного изменив его.

 

Изменение docker-compose.yml

Исходный файл выглядел так:

services:  
  test-bot:  
    build: .  
    container_name: test-bot  
    environment:  
      - BOT_TOKEN=${BOT_TOKEN}

Здесь сервис собирает образ локально (build: .). 
Но теперь у нас уже есть готовый образ в реестре, поэтому локальная сборка не нужна.

Чтобы подтянуть образ из реестра, заменяем строку build: . на image: и указываем полный адрес с тегом:

services:  
  test-bot:  
    image: ghcr.io/prodreams/tempproject:latest  
    container_name: test-bot  
    environment:  
      - BOT_TOKEN=${BOT_TOKEN}

 

Стек в Portainer

Возвращаемся в Portainer и переходим в раздел Stacks:

 

Нажимаем Add stack
Откроется страница создания стека. Выбираем вариант Web editor:

 

Вверху указываем название стека. В моём случае это test-bot
Ниже в поле редактора вставляем содержимое нашего docker-compose.yml:

 

Теперь прокручиваем страницу вниз и включаем переключатель Create a Stack webhook
Это создаст специальную ссылку для вебхука — в будущем мы сможем отправлять на неё запросы из CI/CD, чтобы автоматически обновлять проект.

Также в разделе Environment variables добавляем переменную окружения BOT_TOKEN — сюда вписывается токен нашего Telegram-бота.

После этого жмём Deploy the stack:

 

Через несколько секунд нас перенаправит на страницу запущенных стаков:

 

Осталось проверить, что бот работает:

 

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

 

Получение адреса вебхука

Для следующего шага — настройки Workflow деплоя — нам понадобится адрес Webhook для созданного стека.

Находясь на странице Stacks, выбираем наш стек и переходим на страницу его управления:

 

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

Нажимаем Copy link, чтобы скопировать её:

 

Эта ссылка пригодится нам в CI/CD Workflow: мы будем отправлять на неё запрос, чтобы автоматически обновлять проект при новых сборках.


 

Workflow деплоя

Переходим к самому интересному — деплою проекта через Webhook Portainer в CI/CD.

 

Добавляем секрет в GitHub Actions

Прежде чем писать Workflow, нужно сохранить ссылку вебхука в секрете GitHub. 
Это важно по двум причинам:

  • ссылка не попадёт в открытый доступ;
  • её значение не будет видно в логах Workflow.

Открываем репозиторий на GitHub и заходим в раздел Settings:

 

Слева выбираем Secrets and variables → Actions. Откроется страница управления секретами:

 

Нажимаем зелёную кнопку New repository secret.

В появившейся форме:

  • в поле Name указываем название секрета — PORTAINER_WEBHOOK_URL.
  • в поле Secret вставляем скопированную ранее ссылку вебхука.

 

Нажимаем Add secret — теперь этот секрет можно использовать в Workflow:

 

⚡ Совет: секреты никогда не хранят в коде Workflow напрямую. 
Если вы случайно закоммитите ссылку вебхука в репозиторий, любой сможет деплоить ваш проект.

 

Дополняем build_and_deploy.yaml

Откроем файл build_and_deploy.yaml и добавим новую задачу (job), которая будет выполняться после сборки образа
Она вызовет вебхук, чтобы Portainer автоматически подтянул новый образ и перезапустил проект.

На уровне с build-and-push создаём новый блок deploy:

deploy:  
  runs-on: ubuntu-latest  

  needs: build-and-push  

  steps:  
    - name: Trigger Portainer webhook  
      env:  
        PORTAINER_WEBHOOK_URL: ${{ secrets.PORTAINER_WEBHOOK_URL }}  
      run: curl -fsS -m 30 -X POST "$PORTAINER_WEBHOOK_URL"

Что здесь происходит

  • needs: build-and-push 
    Эта строчка говорит GitHub Actions: 
    «Запусти этот job только после успешного выполнения build-and-push». 
    То есть деплой произойдёт только если образ собрался и загрузился в реестр.
  • env
    В переменную окружения PORTAINER_WEBHOOK_URL передаём значение секрета из GitHub (${{ secrets.PORTAINER_WEBHOOK_URL }}).
    Так мы не храним ссылку вебхука в открытом виде.
  • curl -fsS -m 30 -X POST "$PORTAINER_WEBHOOK_URL" Эта команда отправляет HTTP POST-запрос на вебхук Portainer. Параметры:
    • -f — завершить команду ошибкой, если статус-код ответа ≥ 400;
    • -sS — тихий режим без лишних логов, но с выводом ошибок;
    • -m 30 — ограничение времени выполнения 30 сек.;
    • -X POST — используем POST-запрос;
    • "$PORTAINER_WEBHOOK_URL" — сама ссылка вебхука.

Всего одна команда — и Portainer перезапускает стек с новым образом.

Полный код файла:

name: Build and Deploy Project  

on:  
  push:  
    branches:  
      - main  

permissions:  
  packages: write  
  contents: read  

jobs:  
  build-and-push:  
    runs-on: ubuntu-latest  

    steps:  
      - name: Checkout repository  
        uses: actions/checkout@v4  

      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v3  

      - name: Log in to GitHub Container Registry  
        uses: docker/login-action@v3  
        with:  
          registry: ghcr.io  
          username: ${{ github.actor }}  
          password: ${{ secrets.GITHUB_TOKEN }}  

      - name: Build and push Docker image  
        uses: docker/build-push-action@v6  
        with:  
          context: .  
          push: true  
          cache-from: type=registry,ref=ghcr.io/prodreams/tempproject:latest  
          cache-to: type=inline  
          tags: |  
            ghcr.io/prodreams/tempproject:latest    
            ghcr.io/prodreams/tempproject:${{ github.sha }}  

  deploy:  
    runs-on: ubuntu-latest  

    needs: build-and-push  

    steps:  
      - name: Trigger Portainer webhook  
        env:  
          PORTAINER_WEBHOOK_URL: ${{ secrets.PORTAINER_WEBHOOK_URL }}  
        run: curl -fsS -m 30 -X POST "$PORTAINER_WEBHOOK_URL"

 

Дополняем код бота

Ранее мы уже запустили бота, создав стек в Portainer. 
Чтобы проверить обновление проекта через новый деплой, немного расширим функционал бота и добавим тестовую команду.

Обновлённый код:

import asyncio  
import os  

from aiogram import Dispatcher, Bot  
from aiogram.filters import Command  
from aiogram.types import Message  
from dotenv import load_dotenv  

load_dotenv()  

async def send_message(message: Message) -> None:  
    await message.answer(text="Hello World!")  

async def send_sticker(message: Message) -> None:  
    await message.answer_sticker(sticker="CAACAgIAAxkBAAEKbW1lGVW1I6zFVLyovwo2rSgIt1l35QADJQACYp0ISWYMy8-mubjIMAQ")  

async def start():  
    bot = Bot(token=os.getenv('BOT_TOKEN'))  
    dp = Dispatcher()  

    dp.message.register(send_sticker, Command(commands="test"))  
    dp.message.register(send_message)  

    try:  
        await dp.start_polling(bot)  
    finally:  
        await bot.session.close()  


if __name__  "__main__":  
    asyncio.run(start())

Что изменилось:

  • Добавлена функция send_sticker, которая отправляет стикер.
  • В диспетчер зарегистрирован обработчик:
    • команда /test вызывает send_sticker,
    • любое другое сообщение вызывает send_message и отвечает «Hello World!».

Теперь у нас есть удобный способ проверить, что новая версия бота действительно задеплоилась: достаточно отправить /test и убедиться, что в ответ приходит стикер.

 

Пуш изменений

Осталось последний шаг — отправить изменения в удалённый репозиторий. 
После этого GitHub Actions сразу запустит сборку новой версии образа и деплой через Portainer.

Выполняем знакомые команды:

git add .github/workflows/build_and_deploy.yaml docker-compose.yaml main.py

git commit -m "CI/CD: Deploy"

git push

Теперь заходим в репозиторий на GitHub и открываем раздел Actions:

 

Видим, что запустился Workflow, и теперь в нём выполняются две задачи:

 

Если перейти в подробности задачи deploy, можно убедиться, что всё прошло успешно. 
Обратите внимание: адрес вебхука в логах не отображается — это сделано для безопасности, так как он хранится в GitHub Secrets:

 

Теперь проверим бота:

 

Бот работает корректно!

CI/CD-процесс полностью автоматизирован:

  • при каждом пуше в main собирается новый образ,
  • Portainer подтягивает его и перезапускает стек,
  • а вы сразу можете протестировать новую версию.

 

Заключение

За эти три статьи мы шаг за шагом разобрались, как устроен процесс деплоя: 
от запуска приложения напрямую из Git-репозитория — до полноценного автоматизированного CI/CD.

В чём преимущество Portainer с Webhook

Обычно деплой выглядит так: 
Workflow подключается к серверу по SSH и выполняет там команды для обновления проекта. 
Этот способ подходит для сложных систем, но для большинства небольших проектов он слишком громоздкий и неудобный.

С Portainer всё проще:

  • через удобный интерфейс вы создаёте контейнеры и управляете ими,
  • а благодаря Webhook контейнеры автоматически обновляются после каждой сборки нового образа.

Такой подход особенно ценен для новичков: он позволяет быстро и безопасно внедрить CI/CD без лишней сложности.

Подписывайтесь на наш Telegram‑канал «Код на салфетке» — 
там вы найдёте ещё больше полезных материалов, как для новичков, так и для опытных разработчиков!

Аватар автора

Автор

Иван Ашихмин

Программист, фрилансер и автор гайдов. Занимаюсь разработкой ботов, сайтов и не только.

Войдите, чтобы оставить комментарий.

Комментариев пока нет.