Рано или поздно любой домашний сервер с десятком 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, если не принимать во внимание костыль в виде FerretDB.
Шаг 1. Поднимаем Forgejo - свой git-сервер#
Forgejo - форк Gitea, легкий self-hosted git-сервер с интерфейсом, похожим на GitHub: репозитории, issues, pull request’ы, Actions (CI/CD), встроенный веб-редактор файлов. Для наших целей нужна только базовая функциональность - хранение репозитория и webhooks.
Создаем папку для проекта, например /home/stilicho/docker/forgejo, и в ней файл docker-compose.yml:
services:
server:
image: codeberg.org/forgejo/forgejo:15
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__webhook__ALLOWED_HOST_LIST=external,private
restart: always
networks:
- proxy
volumes:
- ./forgejo:/data
- /etc/localtime:/etc/localtime:ro
ports:
#- '3000:3000'
- '222:22'
labels: # Метки для интеграции с Traefik (обратный прокси)
- "traefik.enable=true" # Включаем обработку контейнера Traefik
- "traefik.http.routers.forgejo.entrypoints=web" # Определяем HTTP-вход (порт 80)
- "traefik.http.routers.forgejo.rule=Host(`forgejo.stilicho.ru`)" # Трафик на этот домен будет направляться в данный контейнер
- "traefik.http.middlewares.forgejo-https-redirect.redirectscheme.scheme=https" # Middleware для редиректа с HTTP на HTTPS
- "traefik.http.routers.forgejo.middlewares=forgejo-https-redirect" # Применяем middleware редиректа к HTTP-маршруту
- "traefik.http.routers.forgejo-secure.entrypoints=websecure" # Определяем HTTPS-вход (порт 443)
- "traefik.http.routers.forgejo-secure.rule=Host(`forgejo.stilicho.ru`)" # HTTPS-маршрут для того же домена
- "traefik.http.routers.forgejo-secure.tls=true" # Включаем TLS (HTTPS)
- "traefik.http.routers.forgejo-secure.service=forgejo" # Привязываем HTTPS-маршрут к сервису forgejo
- "traefik.http.services.forgejo.loadbalancer.server.port=3000" # Указываем внутренний порт, на котором forgejo слушает в контейнере
- "traefik.docker.network=proxy" # Указываем, что Traefik должен искать контейнер в сети "proxy"
security_opt:
- no-new-privileges:true
networks:
proxy:
external: trueРазберем каждый параметр:
USER_UID/USER_GID- под каким пользователем внутри контейнера будет работать Forgejo и, соответственно, каким владельцем будут создаваться файлы в примонтированном томе./forgejo-data. Если у вас на хосте основной пользователь имеет другой UID (проверить можно командойid), лучше подставить его значение - так избежите проблем с правами доступа при бэкапах или ручном редактировании файлов внутриforgejo-data.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), но мы используем обратный прокси Traefik, поэтому у нас доступ будет организован по поддоменному имени. - Порт
222:22- SSH-доступ к git (git clone git@<host>:2222/...). Пробрасываем именно на нестандартный внешний порт222, чтобы не конфликтовать с SSH-демоном самого хоста, который почти наверняка уже слушает22.
Запускаем:
docker compose up -dПроверить, что контейнер поднялся и не падает в перезапуск, можно командой docker compose logs -f forgejo - в первые секунды после старта Forgejo инициализирует структуру данных в /data, это нормально.
Дальше настраиваем через веб-интерфейс:
- Открываем
https://forgejo.stilicho.ru- попадаем в мастер первоначальной настройки. - В мастере большинство полей уже предзаполнены. Стоит обратить внимание на поле 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 на хосте).
Установка ниже - сокращенная версия под конкретно эту связку с Forgejo. Полный разбор с объяснением каждой строчки, входом через Authentik (OIDC) и подключением дополнительных хостов через Periphery - в отдельной статье «Переезжаем с Portainer на Komodo». Здесь повторяю ровно те же конвенции (имена сервисов, файл compose.env, внешние сети), чтобы обе статьи не расходились между собой.
Заводим проект в /home/stilicho/docker/komodo - по тому же принципу, по которому организованы остальные стеки на хосте: все compose-проекты у нас лежат в одной родительской папке /home/stilicho/docker, и Komodo дальше будет видеть их все через эту папку (подробнее - в описании PERIPHERY_ROOT_DIRECTORY ниже).
mkdir -p /home/stilicho/docker/komodo/{mongo/data,mongo/configdb,keys}docker-compose.yaml для Komodo:
services:
mongo:
image: mongo
container_name: komodo-mongo
labels:
komodo.skip:
command: --quiet --wiredTigerCacheSizeGB 0.25
restart: unless-stopped
volumes:
- /home/stilicho/docker/komodo/mongo/data:/data/db
- /home/stilicho/docker/komodo/mongo/configdb:/data/configdb
networks:
- komodo
environment:
MONGO_INITDB_ROOT_USERNAME: ${KOMODO_DATABASE_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${KOMODO_DATABASE_PASSWORD}
core:
image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-2}
container_name: komodo-core
init: true
restart: unless-stopped
depends_on:
- mongo
env_file: ./compose.env
environment:
KOMODO_DATABASE_ADDRESS: mongo:27017
volumes:
- /home/stilicho/docker/komodo/keys:/config/keys
networks:
- proxy
- komodo
security_opt:
- no-new-privileges:true
labels:
- "traefik.enable=true"
- "traefik.http.routers.komodo.entrypoints=web"
- "traefik.http.routers.komodo.rule=Host(`komodo.stilicho.ru`)"
- "traefik.http.middlewares.komodo-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.komodo.middlewares=komodo-https-redirect"
- "traefik.http.routers.komodo-secure.entrypoints=websecure"
- "traefik.http.routers.komodo-secure.rule=Host(`komodo.stilicho.ru`)"
- "traefik.http.routers.komodo-secure.tls=true"
- "traefik.http.routers.komodo-secure.service=komodo"
- "traefik.http.services.komodo.loadbalancer.server.port=9120"
- "traefik.docker.network=proxy"
periphery:
image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-2}
container_name: komodo-periphery
init: true
restart: unless-stopped
depends_on:
- core
env_file: ./compose.env
volumes:
- /home/stilicho/docker/komodo/keys:/config/keys
- /var/run/docker.sock:/var/run/docker.sock
- /proc:/proc
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
networks:
- proxy
- komodo
networks:
proxy:
external: true
komodo:
external: trueОбе сети - внешние, Compose сам их не создаст:
docker network create proxy # если еще не создана для Forgejo/остальных сервисов
docker network create komodoРазберем построчно, что здесь происходит и зачем - те же конвенции, что в статье про Komodo.
mongo#
Данные MongoDB - на bind mount (mongo/data, mongo/configdb), чтобы база физически лежала рядом с остальными файлами проекта, а не пряталась в /var/lib/docker/volumes с автогенерированным именем. Отдельно в сеть proxy этот сервис не добавляем - снаружи он никому, кроме core, не нужен.
komodo.skip:- пустая метка, которая говорит самому Komodo не останавливать этот контейнер при выполнении операции StopAllContainers. Логично: это база данных самого Komodo, ее остановка вместе с остальными стеками сломает саму панель.MONGO_INITDB_ROOT_USERNAME/MONGO_INITDB_ROOT_PASSWORD- создают root-пользователя MongoDB при первой инициализации базы (переменные официального образаmongo, не Komodo). Подставляются через${KOMODO_DATABASE_USERNAME}/${KOMODO_DATABASE_PASSWORD}изcompose.env- см. ниже, почему одногоenv_file:тут недостаточно.
Если у вас уже был запущен mongo без аутентификации - просто добавить MONGO_INITDB_ROOT_USERNAME/PASSWORD в уже существующий контейнер не получится: эти переменные образа mongo применяются только при первой инициализации пустой директории данных. Если mongo/data уже не пустая, потребуется либо очистить ее и поднять базу заново (данные Komodo при этом теряются - для домашнего теста обычно не страшно), либо создать root-пользователя в уже работающей базе вручную через mongosh.
core#
env_file: ./compose.env- подключает переменные из файла, который создадим ниже. Официальный проект делает так же: учетка первого администратора, секреты и публичный ключ Periphery попадают в контейнер именно отсюда, а не построчно вenvironment:.KOMODO_DATABASE_ADDRESS: mongo:27017- единственная переменная, которую оставляем прямо вenvironment:, а не вcompose.env: она специфична для топологии именно этого compose-файла (имя сервиса базы), а не переиспользуется где-то еще.mongo- имя сервиса из этого же файла, Docker DNS резолвит его автоматически внутри сетиkomodo.init: true- включаетtiniкак PID 1 внутри контейнера. Без этого дочерние процессы, которые Core или Periphery порождают при выполнении команд (например,docker composeвнутри Periphery), рискуют оставаться зомби-процессами, если что-то пойдет не так./home/stilicho/docker/komodo/keys:/config/keys- по-настоящему важный момент, который легко упустить. Начиная с Komodo v2 связь между Core и Periphery авторизуется не общим паролем (KOMODO_PASSKEY, как было в v1), а парой Ed25519-ключей, которые каждый компонент генерирует себе сам при первом запуске и складывает в/config/keys. Официальный docker-compose проекта монтирует один и тот же каталогkeysи вcore, и вperiphery, чтобы они могли обменяться публичными ключами через общую файловую систему. Без этого volume Core и Periphery не смогут провести взаимную аутентификацию, и агент не подключится, даже если сеть и адреса настроены идеально.networks: proxy, komodo- Core должен быть одновременно в сетиkomodo(чтобы достучаться доmongoиperiphery) и в сетиproxy(внешняя сеть, в которой сидит Traefik - именно через нее он получает доступ к контейнеру).labels- тот же паттерн, что и у Forgejo из Шага 1 и у всех остальных сервисов сайта за Traefik: HTTP-роутер с редиректом на HTTPS, HTTPS-роутер с TLS иtraefik.docker.network=proxy, чтобы Traefik нашел контейнер именно в сетиproxy.
Название proxy в блоке networks: должно точно совпадать с именем docker-сети, в которой у вас уже работает Traefik. Если у вас эта сеть называется иначе (например, traefik-proxy или web), поправьте имя в обоих местах - иначе Traefik просто не увидит контейнер Komodo, а docker compose up выдаст ошибку о несуществующей внешней сети.
periphery#
Это самая важная для понимания часть.
env_file: ./compose.env- тот же файл, что и уcore, но реально используются из него другие переменные (PERIPHERY_CORE_ADDRESS,PERIPHERY_CORE_PUBLIC_KEYS,PERIPHERY_ROOT_DIRECTORY- разберем их в самомcompose.envниже)./var/run/docker.sock:/var/run/docker.sock- сокет Docker-демона хоста. Без него агент физически не может ни разворачивать, ни останавливать, ни просматривать логи контейнеров - командыdocker compose ..., которые в итоге выполняет Periphery, обращаются именно к этому сокету./proc:/proc- дает Periphery видимость процессов и состояния хоста, а не только своих собственных внутри контейнера. Используется для системной статистики (загрузка CPU, память, диски) на странице сервера в интерфейсе Komodo - без этого mount часть метрик хоста в UI будет пустой или недостоверной.${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}- ключевой bind mount. Путь внутри контейнера и путь на хосте совпадают буква в букву не случайно: Periphery в итоге выполняет командыdocker compose, обращаясь к путям на файловой системе хоста напрямую (через сокет), а не к путям внутри своего контейнера - если пути разъедутся, агент будет “видеть” файлы у себя в контейнере, но не сможет сопоставить их с реальным расположением на хосте, и деплой будет падать с ошибкой “файл не найден”. Значение переменнойPERIPHERY_ROOT_DIRECTORYзадаем вcompose.env- ниже подставим туда/home/stilicho/docker, родительскую папку со всеми стеками./home/stilicho/docker/komodo/keys:/config/keys- тот же самый том, что подключен кcore. Периферии он нужен, чтобы прочитать публичный ключ Core и положить рядом свой собственный - без общего доступа к этой папке подключение отклоняется на этапе аутентификации.networks: proxy, komodo- симметричноcore:komodoдля связи с Core и MongoDB,proxyтоже подключаем (Periphery не обязан быть виден Traefik, но и вреда от этого нет - так сделано в официальном docker-compose).
Если на хосте уже есть папки с работающими стеками - ничего страшного, ничего не сломается. PERIPHERY_ROOT_DIRECTORY - это область видимости, а не команда что-то сделать: она позволяет Periphery видеть compose-проекты в указанной папке (Komodo сможет предложить их в интерфейсе), но сама по себе ничего не запускает, не останавливает и не изменяет. Существующие стеки продолжат работать точно так же, пока вы явно не создадите под них Stack-ресурс в Komodo и не запустите Deploy.
Единственное, о чем стоит помнить: если вы решите взять уже существующий стек под управление через git (привязать к Repo-ресурсу и настроить Pull), Komodo при следующем деплое будет ориентироваться на то, что лежит в репозитории - а не на то, что вы могли поправить руками на диске после последнего коммита. Незапушенные локальные правки в этом случае потеряются при пересоздании контейнера. Поэтому для стеков, переведенных под GitOps, правило простое: с этого момента конфиг правим только через git, не руками на хосте.
compose.env - переменные окружения#
Второй файл, compose.env, хранит все настройки и секреты - обязателен и предусмотрен разработчиками сразу:
nano /home/stilicho/docker/komodo/compose.envCOMPOSE_KOMODO_IMAGE_TAG=2
# Учетные данные MongoDB - должны быть одинаковыми что здесь, что у mongo выше,
# иначе Core не сможет пройти аутентификацию в базе после первого запуска
KOMODO_DATABASE_USERNAME=komodo
KOMODO_DATABASE_PASSWORD=замените_на_свой_пароль
# Первый администратор, создается автоматически при первом запуске Core
KOMODO_LOCAL_AUTH=true
KOMODO_INIT_ADMIN_USERNAME=stilicho
KOMODO_INIT_ADMIN_PASSWORD=замените_на_свой_пароль
# Убирает диалог подтверждения перед каждым действием в интерфейсе -
# удобно для домашней установки с одним оператором
KOMODO_DISABLE_CONFIRM_DIALOG=true
# Секрет для проверки входящих webhook (тот самый, который пригодится в Шаге 4)
KOMODO_WEBHOOK_SECRET=замените_на_длинную_случайную_строку
# Секрет для подписи JWT сессий веб-интерфейса - без него Core сгенерирует
# случайный при каждом рестарте, и все залогиненные сессии будут слетать
KOMODO_JWT_SECRET=замените_на_еще_одну_длинную_случайную_строку
# Публичный ключ Periphery, которому будет доверять Core - сам файл появится
# в общем каталоге keys после первого запуска Periphery, путь просто
# заранее объявляем здесь
KOMODO_PERIPHERY_PUBLIC_KEY=file:/config/keys/periphery.pub
# --- Дальше - переменные, которые реально использует только Periphery,
# но благодаря общему compose.env их не нужно дублировать в двух местах ---
# Как Periphery находит Core: обратите внимание на схему ws://, а не http://,
# это постоянное WebSocket-соединение, а не обычные REST-запросы
PERIPHERY_CORE_ADDRESS=ws://core:9120
# Публичный ключ Core, которому будет доверять Periphery - симметрично полю выше
PERIPHERY_CORE_PUBLIC_KEYS=file:/config/keys/core.pub
# Родительская папка, где лежат ВСЕ ваши compose-проекты (включая komodo,
# но и все остальные) - иначе Periphery не увидит файлы уже существующих
# стеков при их переносе в Komodo
PERIPHERY_ROOT_DIRECTORY=/home/stilicho/dockerПолный список переменных с комментариями - в оригинальном файле на GitHub, здесь оставлены только те, что реально нужны для этой связки.
Как и с traefik.env в статье про Traefik в LXC, этот файл целиком состоит из паролей и секретов - не коммитьте его в git и по возможности сразу ограничьте права:
chmod 600 /home/stilicho/docker/komodo/compose.envЗапуск#
Прежде чем поднимать контейнеры - симлинк, чтобы Docker Compose автоматически подхватывал переменные для подстановки ${...} в самом compose-файле (в mongo выше) без флага --env-file:
cd /home/stilicho/docker/komodo
ln -s compose.env .env
docker compose up -dЕсли пропустить симлинк и запустить docker compose up -d без --env-file compose.env - Compose не подставит KOMODO_DATABASE_USERNAME/PASSWORD в блок environment: сервиса mongo, вы увидите warning про пустые значения, и MongoDB инициализируется с пустыми credentials. Если это уже произошло - нужно снести данные и перезапустить:
docker compose down
rm -rf mongo/data/* mongo/configdb/*
docker compose up -dПервый запуск может занять чуть больше времени - MongoDB инициализирует структуру данных, а Core ждет, пока база станет доступна (depends_on гарантирует только порядок запуска контейнеров, не готовность самой БД принимать соединения - если Core с первой попытки не подключится, он должен переподключиться сам в течение нескольких секунд; если нет - docker compose restart core).
Открываем https://komodo.stilicho.ru и логинимся под учеткой, которую задали через KOMODO_INIT_ADMIN_USERNAME/KOMODO_INIT_ADMIN_PASSWORD - отдельной формы регистрации при первом запуске уже не будет, пользователь создан автоматически.
Если в инфраструктуре уже есть свой OIDC-провайдер (например, Authentik), Komodo умеет входить через него - это удобно, когда доступ к панели нужно давать не только себе. Настройка подробно разобрана в статье про Komodo, здесь для краткости не повторяю - можно вернуться к этому позже, когда базовая схема уже будет работать.
Шаг 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 (если стеков в репозитории несколько) - применяет то, что подтянул предыдущий шаг. У этого шага поле Target - не выбор конкретного стека из списка, а строка-паттерн (в интерфейсе рядом подсказка:
# Match stacks by name, id, wildcard, or \regex\.). Чтобы шаг подхватывал вообще любой изменившийся стек в репозитории - ставим туда просто*.
- 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 и откат в одну команду.




