Рано или поздно любой домашний сервер с десятком Docker-контейнеров превращается в набор compose-файлов, которые лежат у тебя прямо на хосте и правятся руками с помощью твоего любимого редактора. Работает? Да! Но есть нюанс. Если что-то сломалось после правки, непонятно, что именно изменилось и как вернуть все “взад”. А если стеков много? А Хостов? Правок было десять за месяц? Восстановить историю событий по памяти уже нереально.
В этой статье я постараюсь подробно разобрать, как решить эту проблему с помощью так называемого подхода GitOps - на примере связки Forgejo (собственный git-сервер) и Komodo (панель управления Docker-стеками). Всё будем ставить с нуля, разбирая каждый шаг и каждый параметр конфигурации - этого будет достаточно, чтобы не просто скопировать команды, а понимать, что именно они делают и почему.
Если Komodo у вас уже стоит и настроена - например, вы недавно переезжали на неё с Portainer, я разбирал это отдельно в статье «Переезжаем с Portainer на Komodo: полная установка и настройка» - эту часть можно пропустить и начинать прямо с шага про Forgejo ниже. А если, наоборот, интересен не абстрактный туториал с нуля, а то, как именно эта связка живёт у меня в реальной инфраструктуре - с настоящими доменами, переносом уже существующих стеков в git и вебхуком, который уже работает на проде, - об этом отдельная статья: «Forgejo + Komodo: превращаем Docker Compose в GitOps».
Статья рассчитана на тех, кто раньше не сталкивался с GitOps и/или с Forgejo и Komodo по отдельности. Предполагаю, что базовые знания Docker у пользователя есть..
Что такое GitOps и зачем он в homelab#
Классический способ управления Docker-стеками выглядит так:
flowchart LR
```
A[Правите docker-compose.yml на сервере] --> B[docker compose up -d]
B --> C{Что-то сломалось?}
C -->|Да| D[Пытаетесь вспомнить, что меняли]
C -->|Нет| E[Всё работает, пока не забудете]
```
Проблема в том, что сервер сам по себе - не источник правды (иностранный термин, но кладезь информации звучит еще хуже). История изменений живёт только у вас в голове (или не живёт вовсе, если головы нет). Предположим ты правил конфиг в 23:00 после тяжелого рабочего дня, как песни банды Зе Битлз, а через неделю что-то сломалось - шансы понять и вспомнить точную причину стремятся к нулю.
GitOps переворачивает эту схему: единственным источником (да-да, опять) правды становится git-репозиторий. Все compose-файлы лежат в нём, а на сервере работает механизм, который:
- Получает уведомление о новом коммите (через webhook) или сам периодически проверяет репозиторий;
- Подтягивает изменения (
git pull); - Применяет их - пересоздаёт нужные контейнеры (
docker compose up -d).
flowchart LR ``` A[Меняете compose-файл локально] --> B[git commit + push] B --> C[Репозиторий получает webhook] C --> D[Komodo делает Pull + Deploy] D --> E[Контейнеры обновлены] ```
Важный нюанс: сервер сам по себе никогда не принимает решение, что именно должно быть развёрнуто. Он просто синхронизирует своё состояние с тем, что записано в git. Если файл в репозитории не менялся - ничего и не произойдёт, даже если процедура деплоя запустится вручную ещё раз.
Что это даёт на практике:
- История изменений.
git logпоказывает, кто, когда и что поменял, аgit diff- точно что именно изменилось между любыми двумя версиями. - Откат в один шаг. Ошиблись -
git revert, push, и старая версия конфига снова развёрнута, без ручного редактирования файлов на сервере. - Единая точка правды. Не нужно помнить, на каком именно хосте лежит актуальная версия файла - она всегда там, в репозитории.
- Воспроизводимость. Если сервер придётся поднимать заново (авария, переезд на новое железо), весь набор конфигов уже описан в git - не нужно вспоминать, что и как было настроено.
Для homelab с одним сервером выгода не так очевидна, как в продакшене с командой из десяти инженеров - но привычка появится быстро, а откатить неудачный эксперимент одной командой действительно удобно, особенно если ты, как и многие в домашней инфраструктуре, экспериментируешь с конфигами регулярно.
Состав ингредиентов#
- хост с установленными Docker;
- немного свободного места на диске - Forgejo и Komodo сами по себе лёгкие сервисы, основной расход места придётся на данные MongoDB и историю git-репозиториев;
- (опционально, но рекомендуется) уже настроенный реверс-прокси вроде Traefik, если хотите вешать сервисы на собственные домены вместо голых портов - в статье будет пример именно с ним.
Для базы данных Forgejo используем встроенный SQLite - этого более чем достаточно для домашнего использования и позволяет не отвлекаться на настройку внешнего PostgreSQL на старте. Для Komodo Core потребуется MongoDB - это единственная официально поддерживаемая СУБД для Core.
Шаг 1. Поднимаем Forgejo - свой git-сервер#
Forgejo - форк Gitea, лёгкий self-hosted git-сервер с интерфейсом, похожим на GitHub: репозитории, issues, pull request’ы, Actions (CI/CD), встроенный веб-редактор файлов. Для наших целей нужна только базовая функциональность - хранение репозитория и webhooks.
Создаём папку для проекта, например /home/stilicho/docker/forgejo, и в ней файл docker-compose.yml:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:15
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__database__DB_TYPE=sqlite3
- FORGEJO__webhook__ALLOWED_HOST_LIST=external,private
restart: unless-stopped
volumes:
- ./forgejo-data:/data
ports:
- "3000:3000"
- "2222:22"Разберём каждый параметр:
USER_UID/USER_GID- под каким пользователем внутри контейнера будет работать Forgejo и, соответственно, каким владельцем будут создаваться файлы в примонтированном томе./forgejo-data. Если у вас на хосте основной пользователь имеет другой UID (проверить можно командойid), лучше подставить его значение - так избежите проблем с правами доступа при бэкапах или ручном редактировании файлов внутриforgejo-data.FORGEJO__database__DB_TYPE=sqlite3- явно указываем использовать SQLite. Без этой переменной Forgejo в интерактивном мастере предложит выбор БД, но с ней шаг выбора в мастере уже будет предзаполнен.FORGEJO__webhook__ALLOWED_HOST_LIST=external,private- без этой переменной у Forgejo включена защита от SSRF, и по умолчанию она пропускает webhook-запросы только на публичные IP-адреса (external). Если Komodo крутится в той же локальной сети, что и Forgejo (а в домашней инфраструктуре так почти всегда и есть), доменkomodo.stilicho.ruбудет резолвиться в адрес из локальной сети (192.168.x.xи т.п.) - и без ключевого словаprivateв списке Forgejo будет отклонять доставку webhook с ошибкой видаwebhook can only call allowed HTTP servers ... deny 'komodo.stilicho.ru(192.168.x.x:443)', даже если сам webhook настроен полностью верно../forgejo-data:/data- bind mount, в котором Forgejo хранит вообще всё: базу данных SQLite, репозитории, конфиги, вложения issues. Это единственная папка, которую достаточно бэкапить, чтобы иметь полную копию инстанса.- Порт
3000- веб-интерфейс (HTTP). - Порт
2222:22- SSH-доступ к git (git clone git@<host>:2222/...). Пробрасываем именно на нестандартный внешний порт2222, чтобы не конфликтовать с SSH-демоном самого хоста, который почти наверняка уже слушает22.
Запускаем:
docker compose up -dПроверить, что контейнер поднялся и не падает в перезапуск, можно командой docker compose logs -f forgejo - в первые секунды после старта Forgejo инициализирует структуру данных в /data, это нормально.
Дальше настраиваем через веб-интерфейс:
- Открываем
http://<адрес-хоста>:3000- попадаем в мастер первоначальной настройки. Если позже захотите повесить Forgejo на собственный домен через Traefik (как будет показано ниже для Komodo), это делается аналогичным способом - добавлениемlabelsв compose-файл. - В мастере большинство полей уже предзаполнены (в том числе тип БД - благодаря переменной окружения выше). Стоит обратить внимание на поле Forgejo Base URL - если планируете доступ через домен, впишите его сюда сразу, чтобы избежать перенастройки позже (ссылки на клонирование репозиториев в интерфейсе строятся именно из этого значения).
- Пролистываем до раздела Administrator Account Settings - здесь создаём первого пользователя. Он автоматически станет администратором всего инстанса (в нашем примере - логин
stilicho). - Жмём Install Forgejo внизу страницы и ждём завершения инициализации - обычно занимает несколько секунд.
- Входим под только что созданным аккаунтом.
Создаём тестовый репозиторий: значок + в правом верхнем углу → New Repository. Заполняем форму:
- Repository Name - например,
youtube(репозиторий, где будут храниться compose-файлы для стеков); - Visibility - обязательно отмечаем Private, если в репозиторий планируется класть
.env-файлы с секретами (паролями, токенами); - галочку Initialize Repository стоит поставить - так сразу появится
README.md, и репозиторий не будет полностью пустым (в пустой репозиторий немного неудобнее делать первыйgit clone/push).
На этом этапе полезно сразу положить в репозиторий один простой compose-файл - например, разворачивающий тестовый контейнер nginx или traefik/whoami. Его мы будем деплоить через Komodo на следующих шагах, чтобы проверить всю цепочку на живом примере, а не абстрактно.
Шаг 2. Поднимаем Komodo#
Komodo - панель для управления Docker-стеками на одном или нескольких хостах, с поддержкой git-репозиториев как источника compose-файлов и автоматизации через Procedures. Состоит из трёх компонентов: Core (веб-интерфейс и API, “мозг” системы), база данных (MongoDB) и Periphery (агент, который непосредственно выполняет команды Docker на хосте).
Заводим проект в /home/stilicho/docker/komodo - по тому же принципу, по которому организованы остальные стеки на хосте: все compose-проекты у нас лежат в одной родительской папке /home/stilicho/docker, и Komodo дальше будет видеть их все через эту папку (подробнее - в описании PERIPHERY_ROOT_DIRECTORY ниже).
docker-compose.yml для Komodo:
services:
komodo-core:
image: ghcr.io/moghtech/komodo-core:latest
container_name: komodo-core
restart: unless-stopped
environment:
- KOMODO_DATABASE_ADDRESS=komodo-db:27017
- KOMODO_DISABLE_CONFIRM_DIALOG=true
- KOMODO_WEBHOOK_SECRET=CHANGE_ME_TO_A_LONG_RANDOM_STRING
labels:
- "traefik.enable=true"
- "traefik.http.routers.komodo.rule=Host(`komodo.stilicho.ru`)"
- "traefik.http.routers.komodo.entrypoints=websecure"
- "traefik.http.routers.komodo.tls.certresolver=letsencrypt"
- "traefik.http.services.komodo.loadbalancer.server.port=9120"
networks:
- komodo
- proxy
depends_on:
- komodo-db
komodo-db:
image: mongo:7
container_name: komodo-db
restart: unless-stopped
volumes:
- ./komodo-db:/data/db
networks:
- komodo
komodo-periphery:
image: ghcr.io/moghtech/komodo-periphery:latest
container_name: komodo-periphery
restart: unless-stopped
network_mode: host
environment:
- PERIPHERY_ROOT_DIRECTORY=/home/stilicho/docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/stilicho/docker:/home/stilicho/docker
networks:
komodo:
proxy:
external: trueРазберём построчно, что здесь происходит и зачем.
komodo-core#
KOMODO_DATABASE_ADDRESS=komodo-db:27017- адрес MongoDB внутри Docker-сетиkomodo. Обратите внимание: имя хостаkomodo-db- это имя сервиса из этого же compose-файла, Docker DNS резолвит его автоматически внутри общей сети, отдельно ничего прописывать не нужно.KOMODO_DISABLE_CONFIRM_DIALOG=true- отключает всплывающее окно подтверждения перед каждым действием (деплой, рестарт и т.д.) в веб-интерфейсе. Удобно для домашнего использования, где вы и так единственный оператор; в команде, где действия совершают несколько человек, диалог подтверждения может, наоборот, стоит оставить включённым.KOMODO_WEBHOOK_SECRET- секрет, которым Komodo проверяет подлинность входящих webhook-запросов от git-провайдера (по подписиX-Hub-Signature-256). Это один общий секрет на весь инстанс Core, а не отдельное значение под каждый Repo/Stack/Procedure - при настройке webhook в Forgejo для любого ресурса вы будете указывать именно это значение. Сгенерировать случайную строку можно, например, командойopenssl rand -hex 32.traefik.enable=trueи последующиеlabels- вместо того, чтобы пробрасывать порт9120наружу напрямую, отдаём маршрутизацию Traefik. СтрокаHost(\komodo.stilicho.ru`)- правило, по какому домену Traefik должен направлять трафик именно на этот контейнер;certresolver=letsencrypt- какой резолвер сертификатов использовать (имя должно совпадать с тем, что уже настроено в вашем Traefik);loadbalancer.server.port=9120` - на какой внутренний порт контейнера направлять запросы (это порт, на котором слушает сам Komodo Core, он не меняется).networks: komodo, proxy- Core должен быть одновременно в сетиkomodo(чтобы достучаться доkomodo-db) и в сетиproxy(это внешняя сеть, в которой сидит ваш Traefik - именно через неё Traefik получает доступ к контейнеру для маршрутизации).
Название proxy в блоке networks: proxy: external: true должно точно совпадать с именем docker-сети, в которой у вас уже работает Traefik. Если у вас эта сеть называется иначе (например, traefik-proxy или web), обязательно поправьте это имя в обоих местах - иначе Traefik просто не увидит контейнер Komodo и маршрутизация не заработает, а docker compose up выдаст ошибку о несуществующей внешней сети.
komodo-db#
Ничего специфичного - обычный контейнер MongoDB 7 с данными на bind mount ./komodo-db:/data/db, чтобы база физически лежала рядом с остальными файлами проекта, а не пряталась в /var/lib/docker/volumes с автогенерированным именем. Отдельно в сеть proxy этот сервис не добавляем - снаружи он никому, кроме komodo-core, не нужен.
komodo-periphery#
Это самая важная для понимания часть.
network_mode: host- Periphery работает в сетевом пространстве самого хоста, а не в изолированной Docker-сети. Это осознанный выбор: агенту нужно уметь взаимодействовать с Docker так же, как это делали бы вы сами по SSH, и режимhostизбавляет от лишних сложностей с проброской портов между агентом и Core.PERIPHERY_ROOT_DIRECTORY=/home/stilicho/docker- ключевая переменная. Она указывает Periphery-агенту на родительскую папку, где лежат директории всех остальных стеков (не только Komodo). Именно благодаря ей Komodo видит и умеет управлять уже существующими compose-проектами на хосте, а не только тем, что создан специально под неё. По сути, это “корень”, от которого Komodo строит все остальные пути при работе со Stack-ресурсами./var/run/docker.sock:/var/run/docker.sock- сокет Docker-демона хоста, примонтированный внутрь контейнера Periphery. Без него агент физически не имеет возможности ни разворачивать, ни останавливать, ни просматривать логи каких-либо контейнеров - вся командаdocker compose ..., которую в итоге выполняет Periphery, обращается именно к этому сокету./home/stilicho/docker:/home/stilicho/docker- второй bind mount, дающий контейнеру доступ непосредственно к файлам на диске (compose-файлам,.env, любым вспомогательным файлам стеков).
Если у вас, как и здесь, Periphery работает в network_mode: host, то бинд-маунт /home/stilicho/docker:/home/stilicho/docker должен указывать на тот же путь, что и PERIPHERY_ROOT_DIRECTORY - то есть путь внутри контейнера и путь на хосте совпадают буква в букву. Причина в том, что Periphery в итоге выполняет команды docker compose, обращаясь к путям на файловой системе хоста напрямую (через сокет), а не к путям внутри своего контейнера - если пути разъедутся, агент будет “видеть” файлы у себя в контейнере, но не сможет сопоставить их с реальным расположением на хосте, и деплой будет падать с ошибкой “файл не найден” или похожей.
Если на хосте уже есть папки с работающими стеками - ничего страшного, ничего не сломается. PERIPHERY_ROOT_DIRECTORY - это область видимости, а не команда что-то сделать: она позволяет Periphery видеть compose-проекты в указанной папке (Komodo сможет предложить их в интерфейсе), но сама по себе ничего не запускает, не останавливает и не изменяет. Существующие стеки продолжат работать точно так же, как работали, пока вы явно не создадите под них Stack-ресурс в Komodo и не запустите Deploy.
Единственное, о чём стоит помнить: если вы решите взять уже существующий стек под управление через git (привязать к Repo-ресурсу и настроить Pull), Komodo при следующем деплое будет ориентироваться на то, что лежит в репозитории - а не на то, что вы могли поправить руками на диске после последнего коммита. Незапушенные локальные правки в этом случае потеряются при пересоздании контейнера. Поэтому для стеков, переведённых под GitOps, правило простое: с этого момента конфиг правим только через git, не руками на хосте.
Запускаем:
docker compose up -dПервый запуск может занять чуть больше времени - MongoDB инициализирует структуру данных, а Core ждёт, пока база станет доступна (за это отвечает depends_on, но он гарантирует только порядок запуска контейнеров, не готовность самой БД принимать соединения - если Core с первой попытки не подключится, он должен переподключиться сам в течение нескольких секунд; если нет - docker compose restart komodo-core).
Открываем https://komodo.stilicho.ru (или http://<адрес-хоста>:9120, если решили пока обойтись без Traefik и оставили порт проброшенным напрямую), создаём первого пользователя-администратора в форме, которая появляется при первом заходе.
Если в инфраструктуре уже есть свой OIDC-провайдер (например, Authentik), Komodo умеет входить через него - это удобно, когда доступ к панели нужно давать не только себе. Настраивается отдельными переменными окружения для komodo-core (адрес провайдера, client ID/secret) и не является обязательным для первого запуска - можно вернуться к этому позже, когда базовая схема уже будет работать.
Шаг 3. Связываем Komodo с репозиторием в Forgejo#
Чтобы Komodo могла клонировать приватный репозиторий (а публичный, кстати, тоже - Forgejo по умолчанию требует авторизацию даже для чтения по HTTP, если явно не открыть анонимный доступ), нужен токен доступа.
Создаём токен в Forgejo#
Заходим под своим пользователем (
stilicho) в Forgejo → значок профиля в правом верхнем углу → Settings.В левом меню выбираем Applications.
В разделе Manage Access Tokens заполняем Token Name (например,
komodo-readonly).В блоке Repository and Organization Access - это нововведение именно 15-й версии - выбираем один из трёх вариантов:
- All - токен получит доступ ко всем репозиториям аккаунта (публичным, приватным, с ограниченным доступом);
- Public only - доступ ограничен публичными репозиториями;
- Specific repositories - доступ только к явно выбранным репозиториям.
Для нашей задачи логичнее всего взять Specific repositories и выбрать в появившемся списке только
youtube- токен, скомпрометированный или случайно засвеченный где-то ещё, в этом случае не даст доступа ни к чему, кроме одного этого репозитория.При выборе Specific repositories доступны только четыре права:
read:repository,write:repository,read:issue,write:issue(остальные скоупы для точечного токена Forgejo просто не показывает - им не с чем сверяться, если токен не привязан к конкретному репозиторию). Для клонирования иpullсо стороны Komodo достаточноread:repository;write:repositoryберём дополнительно, только если планируете и пушить в этот репозиторий тем же токеном (например, для автоматических коммитов из CI).Жмём Generate Token.
Токен показывается один единственный раз, сразу после генерации. Скопируйте его сразу - если закроете страницу или обновите её, посмотреть токен повторно будет уже нельзя, придётся генерировать новый.
Регистрируем токен в Komodo#
Komodo не привязывает токен к конкретному репозиторию напрямую - вместо этого сначала регистрируется учётная запись у git-провайдера (связка “домен + логин + токен”), а уже она потом используется при создании любого количества Repo-ресурсов.
- В интерфейсе Komodo идём в Settings (иконка шестерёнки) → раздел Providers.
- В блоке Git Providers жмём Add Provider (или +, в зависимости от версии интерфейса). Заполняем:
- Domain - домен вашего Forgejo без протокола и без
http(s)://, напримерforgejo.stilicho.ru(или<адрес-хоста>:3000, если работаете без домена); - HTTPS - переключатель, включён по умолчанию; если Forgejo пока не за Traefik и доступна только по
http://, обязательно выключите этот тумблер - иначе Komodo будет пытаться клонировать поhttps://и получит ошибку соединения; - внутри провайдера добавляем аккаунт (Add Account): Username - ваш логин в Forgejo (
stilicho), Token - токен, скопированный на предыдущем шаге.
- Domain - домен вашего Forgejo без протокола и без
- Сохраняем - провайдер и привязанный к нему аккаунт появятся в списке.
На стороне Komodo токен привязывается не к конкретному Repo-ресурсу, а к домену провайдера в целом - один раз добавили Forgejo как провайдер и аккаунт stilicho, и дальше при создании новых Repo-ресурсов для других репозиториев на том же Forgejo просто выбираете этот аккаунт из списка, а не копируете токен заново в каждую форму. А вот на стороне самого Forgejo, как мы только что видели, токен в v15 можно ограничить конкретным репозиторием - это два независимых уровня ограничения, и они друг другу не мешают.
Создаём Repo-ресурс#
- Resources → Repos → Create Repo.
- Даём ресурсу понятное имя (например, `youtube).
- В поле Repo указываем только
владелец/репозиторий, без протокола и домена - в нашем случаеstilicho/youtube. Домен Komodo подставит сама из выбранного на следующем шаге Git-провайдера; если вписать сюда ещё и полный URL (https://forgejo.stilicho.ru/stilicho/youtube.git), домен задвоится и клонирование упадёт с ошибкойremote: Not found- Komodo в буквальном смысле склеит<домен провайдера>/<то, что вы ввели>. - В поле Git Account выбираем из выпадающего списка аккаунт
stilichoна провайдере, добавленном на предыдущем шаге - сам токен здесь заново вводить не нужно, Komodo подставит его автоматически при клонировании. - Ветку (Branch) оставляем
main(илиmaster, в зависимости от того, как Forgejo назвала ветку по умолчанию при создании репозитория - это видно на странице репозитория). - Сохраняем.
Проверить, что клонирование прошло успешно, можно на странице самого Repo-ресурса - там есть история выполненных операций (Pull, Clone) с логами, включая точную команду git clone, которую Komodo выполнила. Полезно проверить ее выполнение руками хотя бы один раз - так виднее, что именно подставилось вместо домена и токена. Если авторизация не прошла, а лог покажет ошибку 401/403 - стоит перепроверить, что токен скопирован без лишних пробелов и что права токена включают чтение репозитория. Если же ошибка вида remote: Not found / repository ... not found при, казалось бы, верном токене - почти наверняка домен в адресе задвоился именно из-за полного URL в поле Repo (см. предупреждение выше).
Если на хосте уже есть несколько работающих стеков (как в примере выше, где PERIPHERY_ROOT_DIRECTORY указывает на общую папку /home/stilicho/docker), удобнее не заводить отдельный репозиторий под каждый стек, а держать один общий монорепозиторий - с подпапкой на каждый стек внутри. Так все compose-файлы видны Komodo сразу через один Repo-ресурс, и не приходится плодить десятки отдельных записей.
.env-файлы в таком репозитории можно коммитить как есть, но обязательно делайте репозиторий приватным - иначе секреты (пароли БД, токены) окажутся доступны кому угодно, у кого есть ссылка на репозиторий.
Создаём Stack#
- Resources → Stacks → Create Stack.
- Даём стеку имя (например,
traefik, если это стек с реверс-прокси, илиnginx-testдля тестового примера). - Server - на каком хосте (Periphery-агенте) разворачивать стек. Если у вас пока один хост - выбирать особо не из чего, но при добавлении второго хоста через отдельный Periphery-агент это поле определяет, куда именно уедет деплой.
- Select Repo - здесь либо выбираем ранее созданный Repo-ресурс, либо (если отдельный Repo-ресурс не заводили) настраиваем источник прямо тут же: Git Provider → Account → Repo (формат тот же -
владелец/репозиторий, напримерstilicho/youtube) → Branch. Оба пути равнозначны, просто отдельный Repo-ресурс удобнее переиспользовать в Procedure, как показано ниже. - Run Directory - путь до подпапки с compose-файлом внутри репозитория, например
traefik, относительно корня репозитория. - File Paths - имя самого compose-файла, которое будет подставлено в
docker compose -f, относительноRun Directory. Если оставить пустым, Komodo подставитcompose.yaml- если ваш файл называется иначе (например, привычнее многимdocker-compose.yml), это обязательно нужно прописать явно в этом поле, иначе деплой упадёт на шаге валидации с ошибкой видаMissing files: compose.yaml, даже если сам файл на месте, просто под другим именем. - Сохраняем и жмём Deploy - Komodo должна поднять контейнер(ы) из вашего compose-файла.
Там же, чуть ниже, есть поле Clone Path - по умолчанию Komodo клонирует репозиторий в $root_directory/stacks/<имя стека> (в нашем случае это будет /home/stilicho/docker/stacks/traefik), и в 99% случаев трогать это поле не нужно. Полезно просто знать, что оно означает - именно этот путь вы увидите в логе шага Clone Repo, если понадобится туда зайти руками и что-то проверить.
Run Directory и File Paths - это два независимых поля, и обе ошибки выглядят очень похоже в логе (ERROR: A file doesn't exist after writing stack / Missing files: ...), но чинятся по-разному:
- если репозиторий склонировался, но нужной подпапки внутри него как будто нет - проверяйте Run Directory;
- если подпапка верная, но написано, что не хватает конкретно
compose.yaml- проверяйте File Paths: скорее всего, ваш файл называетсяdocker-compose.ymlили как-то ещё, и нужно указать это имя явно, а не полагаться на дефолт.
Проверить результат можно двумя способами: посмотреть логи деплоя прямо на странице Stack-ресурса в Komodo (там видно каждый шаг - Clone Repo, Latest Commit, Validate Files, Deploy Compose - и на каком именно из них что-то пошло не так), либо зайти на хост и выполнить docker ps - контейнер должен появиться в списке с именем, соответствующим сервисам из compose-файла.
Если контейнер запустился - связка репозиторий → Komodo → Docker уже работает вручную. Осталось убрать последний ручной шаг - нажатие кнопки Deploy.
Шаг 4. Автодеплой по push#
Здесь и происходит основная идея GitOps: вместо того чтобы каждый раз заходить в Komodo и жать Deploy, мы хотим, чтобы это происходило само при пуше в репозиторий.
Procedure в Komodo#
Procedure - это последовательность из одного или нескольких шагов (Pull, Deploy, отправка уведомления и т.д.), которую можно запустить одной командой или по триггеру, например по webhook.
- Resources → Procedures → Create Procedure.
- Даём процедуре имя, например
deploy-on-push. - Добавляем шаги по порядку (кнопка Add Step, либо Add Stage в зависимости от версии интерфейса - шаги внутри Procedure выполняются строго сверху вниз):
- Pull Repo - указываем тот же Repo-ресурс, что и в Stack. Этот шаг подтягивает последний коммит из ветки
mainв локальную рабочую копию на хосте. - Deploy Stack (если стек один) либо Batch Deploy Stack If Changed (если стеков в репозитории несколько) - применяет то, что подтянул предыдущий шаг.
- Pull Repo - указываем тот же Repo-ресурс, что и в Stack. Этот шаг подтягивает последний коммит из ветки
- Сохраняем Procedure.
Вариант “If Changed” полезен, если в вашем монорепозитории лежит сразу несколько стеков: Komodo сравнивает содержимое папки каждого стека до и после Pull и передеплоивает только те, чьи файлы реально изменились в последнем коммите, а не пересоздаёт все контейнеры разом при любом чихе в репозитории.
Если стек в репозитории всего один, Procedure вообще не обязательна: у каждого Stack-ресурса на его собственной странице тоже есть готовый раздел Webhooks с URL вида .../listener/github/stack/<id>/deploy - можно навесить webhook сразу на него, без промежуточной Procedure. Сама Deploy-операция стека уже включает в себя актуализацию из git, отдельный шаг Pull для неё не нужен. Procedure с явными Pull + Batch Deploy имеет смысл именно тогда, когда одним пушем в монорепозиторий нужно затронуть несколько разных стеков одновременно.
Webhook в Forgejo#
Формат webhook-адреса в Komodo фиксированный:
https://<HOST>/listener/<AUTH_TYPE>/<RESOURCE_TYPE>/<ID_OR_NAME>/<EXECUTION>Для Forgejo/Gitea AUTH_TYPE - всегда github (Komodo проверяет подпись X-Hub-Signature-256, тот же формат, что использует GitHub, и Forgejo умеет отправлять webhook именно в нём). Для ресурса типа Procedure EXECUTION - это не команда вроде “deploy”, а имя ветки, на пуш в которую должна реагировать процедура (или __ANY__, чтобы реагировать на пуш в любую ветку); для Stack это как раз конкретная команда - /deploy или /refresh.
Открываем страницу Procedure
deploy-on-push(или страницу конкретного Stack, если решили обойтись без Procedure) в Komodo - в разделе Webhooks там уже будет готовый URL, собирать вручную ничего не придётся. Для Procedure это будет выглядеть так:https://komodo.stilicho.ru/listener/github/procedure/deploy-on-push/mainВ Forgejo идём в нужный репозиторий → Settings → Webhooks → Add Webhook → Forgejo (Forgejo поддерживает свой нативный формат webhook, отдельный от универсального Gitea-совместимого - выбирайте именно Forgejo, если он есть в списке; если нет, подойдёт и Gitea - формат событий совместим и с тем, и с другим).
Заполняем форму:
- Target URL - вставляем собранный URL из шага 1;
- HTTP Method -
POST; - POST Content Type -
application/json; - Secret - по умолчанию вставляем значение
KOMODO_WEBHOOK_SECRET, которое вы задали в compose-файле Komodo на шаге 2 (это глобальный секрет, общий для всех webhook на все ресурсы инстанса). У каждого ресурса в разделе Webhooks есть поле Webhook Secret, которым при желании можно задать свой отдельный секрет именно для него вместо глобального - но для домашней установки с одним пользователем в этом обычно нет необходимости; - Trigger On - выбираем событие Push Events (можно оставить только его - остальные события вроде issues или pull request’ов для нашей задачи не нужны).
Сохраняем webhook. Forgejo сразу предложит отправить тестовый Test Delivery - стоит им воспользоваться: если Komodo ответит
200 OK, значит секрет и URL настроены верно, и можно переходить к проверке на реальном коммите.
Если в ответ на Test Delivery вместо кода ответа видите красный крестик и текст вида webhook can only call allowed HTTP servers ... deny '<домен>(<IP>:443)' - это не проблема с секретом или URL, это защита Forgejo от SSRF: по умолчанию она блокирует webhook на адреса из локальной сети. Убедитесь, что в переменной FORGEJO__webhook__ALLOWED_HOST_LIST контейнера Forgejo (см. compose-файл в Шаге 1) есть значение private - без него любой webhook на домен, который резолвится в 192.168.x.x/10.x.x.x, будет отклонён именно с такой ошибкой, сколько бы раз вы ни проверяли секрет и URL.
Если в имени вашей Procedure есть пробелы или отличающийся от ID регистр, надёжнее подставить в URL числовой ID ресурса вместо имени - его можно скопировать со страницы ресурса в Komodo. Имя может со временем поменяться (вы захотите переименовать процедуру), а вот ID - никогда, так что ссылка в настройках webhook Forgejo не “отвяжется” от ресурса при переименовании.
Проверка#
Все команды git ниже выполняются на вашем компьютере, в локальной копии репозитория - не на сервере, где стоят Forgejo и Komodo. Сервер только принимает push и реагирует на него через webhook.
Если репозитория ещё нет локально, клонируем его (URL - тот же, что использовали при создании Repo-ресурса в Komodo; при первом обращении git спросит логин и пароль - в качестве пароля используем тот же токен, что регистрировали в Komodo, либо заводим для себя отдельный личный токен с правами на запись):
git clone http://<адрес-хоста>:3000/stilicho/youtube.git
cd youtubeДальше меняем что-нибудь в нужном compose-файле - например, версию образа - и пушим:
git add .
git commit -m "bump nginx version"
git pushИзменить файл можно и прямо в веб-интерфейсе Forgejo (открыть файл → карандаш Edit → внизу страницы Commit Changes) - тогда локальный клон вообще не нужен, коммит и push произойдут на стороне сервера автоматически. Для быстрых правок это даже удобнее, чем возиться с git-клиентом на своей машине.
После git push события развиваются так:
sequenceDiagram ``` participant Вы participant Forgejo participant Komodo participant Docker Вы->>Forgejo: git push Forgejo->>Komodo: webhook (push event) Komodo->>Komodo: Pull Repo Komodo->>Docker: Deploy Stack Docker-->>Komodo: контейнер обновлён ```
Через несколько секунд в разделе Procedures → deploy-on-push в Komodo должно появиться новое выполнение (run) со статусом Success - открыв его, можно увидеть подробный лог обоих шагов: что именно подтянул Pull Repo и что вывел Deploy Stack при пересоздании контейнера. Контейнер при этом пересоздаётся с новым конфигом без единого ручного действия с вашей стороны.
Если выполнение не запустилось автоматически - первым делом стоит проверить лог доставки webhook на стороне Forgejo (Settings → Webhooks → <ваш webhook> → Recent Deliveries): там видно, каким статус-кодом ответила Komodo, и это обычно сразу указывает на причину (неверный секрет - 401, неверный URL - 404 или таймаут соединения).
Проверяем откат#
Ради интереса стоит сразу опробовать и обратную операцию - снова в той же локальной копии репозитория (cd youtube, если ещё не там):
git revert HEAD
git pushgit revert HEAD создаёт новый коммит, который отменяет изменения последнего коммита (в нашем случае - той самой правки версии образа), не переписывая при этом историю - в отличие от git reset, старый коммит остаётся в логе, просто поверх него добавляется отменяющий. Это важно: если между вашей правкой и откатом кто-то (или вы сами) успеет запушить ещё один коммит, HEAD уже будет указывать не на ту правку, которую вы хотели откатить - в такой ситуации точнее откатывать конкретный коммит по хэшу: git revert <hash> вместо git revert HEAD.
После git push тот же webhook сработает снова, Komodo подтянет откаченный коммит и вернёт предыдущую версию стека - контейнер пересоздастся уже со старой версией образа. Это и есть главное практическое преимущество подхода: ошибка исправляется так же просто, как и вносится, двумя командами в терминале, без необходимости заходить на сервер и разбираться, что именно там сейчас развёрнуто.
Что дальше#
Показанная здесь схема - минимальная: один хост, MongoDB и SQLite внутри тех же контейнеров, один пользователь-администратор. Когда захочется расти, есть куда:
- вынести базы данных Forgejo и Komodo на внешний PostgreSQL/MongoDB - полезно, если на хосте уже есть общая СУБД для нескольких сервисов;
- подключить несколько хостов через отдельные Periphery-агенты - Core остаётся один, а Periphery ставится на каждый дополнительный хост и указывает на свой
PERIPHERY_ROOT_DIRECTORY; - добавить CI через Forgejo Actions - например, автоматическую проверку синтаксиса compose-файлов (
docker compose config) перед тем, как коммит вообще попадёт в основную ветку; - настроить оповещения о деплоях - Komodo умеет отправлять уведомления в Discord, Slack, Telegram или на произвольный webhook при завершении Procedure;
- по мере роста числа репозиториев переходить на Specific repositories-токены (как для Forgejo v15 в этой статье) везде, где раньше стоял токен с доступом “ко всему” - так компрометация одного токена не откроет доступ сразу ко всем вашим репозиториям.
Но для первого знакомства с GitOps в homelab хватит и того, что уже есть: репозиторий как источник правды, автоматический деплой по push и откат в одну команду.





