↓ Перейти к основному содержимому
  1. Posts/
  2. Self-Hosting/

GitOps для homelab: разворачиваем Forgejo и Komodo с нуля

··5746 слов·28 минут· loading · loading · ·
Stilicho2011
Автор
Stilicho2011
Пишу о homelab, self-hosting, автоматизации и open-source решениях
Оглавление
Self-Hosting - This article is part of a series.
Part : This Article

Рано или поздно любой домашний сервер с десятком Docker-контейнеров превращается в набор compose-файлов, которые лежат у тебя на хосте и правятся руками с помощью твоего любимого редактора. Работает? Работает! Но есть нюанс. Если что-то сломалось после правки, непонятно, что именно изменилось и как вернуть все “взад”. Ок, хорошо. Если один стэк, то наверно понятно где напортачил. А если стеков много? А хостов? Может быть у тебя правок было десять за месяц? Восстановить историю событий по памяти уже нереально.

[]

В этой статье я постараюсь подробно разобрать, как решить эту проблему с помощью так называемого подхода GitOps - на примере связки Forgejo (собственный git-сервер) и Komodo (панель управления Docker-стеками). Все будем ставить с нуля, разбирая каждый шаг и каждый параметр конфигурации - этого будет достаточно, чтобы не просто скопировать команды неизвестного тебе ютубера, а понимать, что именно эти команды делают и почему.

Если Komodo у вас уже стоит и настроен - например, вы недавно переезжали на нее с Portainer, я разбирал это отдельно в статье «Переезжаем с Portainer на Komodo: полная установка и настройка» - эту часть можно пропустить и начинать прямо с шага про установку Forgejo ниже. А если, наоборот, интересен не абстрактный туториал с нуля, а то, как именно эта связка живет у меня в реальной инфраструктуре - с настоящими доменами, переносом уже существующих стеков в git и вебхуком, который уже работает на проде, - об этом отдельная статья: «Forgejo + Komodo: превращаем Docker Compose в GitOps».

Note

Статья рассчитана на тех, кто раньше не сталкивался с 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-файлы лежат в нем, а на сервере работает механизм, который:

  1. Получает уведомление о новом коммите (через webhook) или сам периодически проверяет репозиторий;
  2. Подтягивает изменения (git pull);
  3. Применяет их - пересоздает нужные контейнеры (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, это нормально.

Дальше настраиваем через веб-интерфейс:

  1. Открываем https://forgejo.stilicho.ru - попадаем в мастер первоначальной настройки.
  2. В мастере большинство полей уже предзаполнены. Стоит обратить внимание на поле Forgejo Base URL - если планируете доступ через домен, впишите его сюда сразу, чтобы избежать перенастройки позже (ссылки на клонирование репозиториев в интерфейсе строятся именно из этого значения).
  3. Пролистываем до раздела Administrator Account Settings - здесь создаем первого пользователя. Он автоматически станет администратором всего инстанса (в нашем примере - логин stilicho).
  4. Жмем Install Forgejo внизу страницы и ждем завершения инициализации - обычно занимает несколько секунд.
  5. Входим под только что созданным аккаунтом.

Создаем тестовый репозиторий: значок + в правом верхнем углу → New Repository. Заполняем форму:

  • Repository Name - например, youtube (репозиторий, где будут храниться compose-файлы для стеков);
  • Visibility - обязательно отмечаем Private, если в репозиторий планируется класть .env-файлы с секретами (паролями, токенами);
  • галочку Initialize Repository стоит поставить - так сразу появится README.md, и репозиторий не будет полностью пустым (в пустой репозиторий чуть неудобнее делать первый git clone/push).
Tip

На этом этапе полезно сразу положить в репозиторий один простой compose-файл - например, разворачивающий тестовый контейнер nginx или traefik/whoami (как я сделал в видео). Его мы будем деплоить через Komodo на следующих шагах, чтобы проверить всю цепочку на живом примере, а не абстрактно.

Шаг 2. Поднимаем Komodo
#

Komodo - панель для управления Docker-стеками на одном или нескольких хостах, с поддержкой git-репозиториев как источника compose-файлов и автоматизации через Procedures. Состоит из трех компонентов: Core (веб-интерфейс и API, “мозг” системы), база данных (MongoDB) и Periphery (агент, который непосредственно выполняет команды Docker на хосте).

Note

Установка ниже - сокращенная версия под конкретно эту связку с 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: тут недостаточно.
Warning

Если у вас уже был запущен 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.
Warning

Название 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).
Warning

Если на хосте уже есть папки с работающими стеками - ничего страшного, ничего не сломается. 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.env
COMPOSE_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 - отдельной формы регистрации при первом запуске уже не будет, пользователь создан автоматически.

Tip

Если в инфраструктуре уже есть свой OIDC-провайдер (например, Authentik), Komodo умеет входить через него - это удобно, когда доступ к панели нужно давать не только себе. Настройка подробно разобрана в статье про Komodo, здесь для краткости не повторяю - можно вернуться к этому позже, когда базовая схема уже будет работать.

Шаг 3. Связываем Komodo с репозиторием в Forgejo
#

Чтобы Komodo могла клонировать приватный репозиторий (а публичный, кстати, тоже - Forgejo по умолчанию требует авторизацию даже для чтения по HTTP, если явно не открыть анонимный доступ), нужен токен доступа.

Создаем токен в Forgejo
#

  1. Заходим под своим пользователем (stilicho) в Forgejo → значок профиля в правом верхнем углу → Settings.

  2. В левом меню выбираем Applications.

  3. В разделе Manage Access Tokens заполняем Token Name (например, komodo-readonly).

  4. В блоке Repository and Organization Access - это нововведение именно 15-й версии - выбираем один из трех вариантов:

    • All - токен получит доступ ко всем репозиториям аккаунта (публичным, приватным, с ограниченным доступом);
    • Public only - доступ ограничен публичными репозиториями;
    • Specific repositories - доступ только к явно выбранным репозиториям.

    Для нашей задачи логичнее всего взять Specific repositories и выбрать в появившемся списке только youtube - токен, скомпрометированный или случайно засвеченный где-то еще, в этом случае не даст доступа ни к чему, кроме одного этого репозитория.

  5. При выборе Specific repositories доступны только четыре права: read:repository, write:repository, read:issue, write:issue (остальные скоупы для точечного токена Forgejo просто не показывает - им не с чем сверяться, если токен не привязан к конкретному репозиторию). Для клонирования и pull со стороны Komodo достаточно read:repository; write:repository берем дополнительно, только если планируете и пушить в этот репозиторий тем же токеном (например, для автоматических коммитов из CI).

  6. Жмем Generate Token.

  7. Токен показывается один единственный раз, сразу после генерации. Скопируйте его сразу - если закроете страницу или обновите ее, посмотреть токен повторно будет уже нельзя, придется генерировать новый.

Регистрируем токен в Komodo
#

Komodo не привязывает токен к конкретному репозиторию напрямую - вместо этого сначала регистрируется учетная запись у git-провайдера (связка “домен + логин + токен”), а уже она потом используется при создании любого количества Repo-ресурсов.

  1. В интерфейсе Komodo идем в Settings (иконка шестеренки) → раздел Providers.
  2. В блоке Git Providers жмем Add Provider (или +, в зависимости от версии интерфейса). Заполняем:
    • Domain - домен вашего Forgejo без протокола и без http(s)://, например forgejo.stilicho.ru (или <адрес-хоста>:3000, если работаете без домена);
    • HTTPS - переключатель, включен по умолчанию; если Forgejo пока не за Traefik и доступна только по http://, обязательно выключите этот тумблер - иначе Komodo будет пытаться клонировать по https:// и получит ошибку соединения;
    • внутри провайдера добавляем аккаунт (Add Account): Username - ваш логин в Forgejo (stilicho), Token - токен, скопированный на предыдущем шаге.
  3. Сохраняем - провайдер и привязанный к нему аккаунт появятся в списке.
Note

На стороне Komodo токен привязывается не к конкретному Repo-ресурсу, а к домену провайдера в целом - один раз добавили Forgejo как провайдер и аккаунт stilicho, и дальше при создании новых Repo-ресурсов для других репозиториев на том же Forgejo просто выбираете этот аккаунт из списка, а не копируете токен заново в каждую форму. А вот на стороне самого Forgejo, как мы только что видели, токен в v15 можно ограничить конкретным репозиторием - это два независимых уровня ограничения, и они друг другу не мешают.

Создаем Repo-ресурс
#

  1. Resources → Repos → Create Repo.
  2. Даем ресурсу понятное имя (например, `youtube).
  3. В поле Repo указываем только владелец/репозиторий, без протокола и домена - в нашем случае stilicho/youtube. Домен Komodo подставит сама из выбранного на следующем шаге Git-провайдера; если вписать сюда еще и полный URL (https://forgejo.stilicho.ru/stilicho/youtube.git), домен задвоится и клонирование упадет с ошибкой remote: Not found - Komodo в буквальном смысле склеит <домен провайдера>/<то, что вы ввели>.
  4. В поле Git Account выбираем из выпадающего списка аккаунт stilicho на провайдере, добавленном на предыдущем шаге - сам токен здесь заново вводить не нужно, Komodo подставит его автоматически при клонировании.
  5. Ветку (Branch) оставляем main (или master, в зависимости от того, как Forgejo назвала ветку по умолчанию при создании репозитория - это видно на странице репозитория).
  6. Сохраняем.

Проверить, что клонирование прошло успешно, можно на странице самого Repo-ресурса - там есть история выполненных операций (Pull, Clone) с логами, включая точную команду git clone, которую Komodo выполнила. Полезно проверить ее выполнение руками хотя бы один раз - так виднее, что именно подставилось вместо домена и токена. Если авторизация не прошла, а лог покажет ошибку 401/403 - стоит перепроверить, что токен скопирован без лишних пробелов и что права токена включают чтение репозитория. Если же ошибка вида remote: Not found / repository ... not found при, казалось бы, верном токене - почти наверняка домен в адресе задвоился именно из-за полного URL в поле Repo (см. предупреждение выше).

Tip

Если на хосте уже есть несколько работающих стеков (как в примере выше, где PERIPHERY_ROOT_DIRECTORY указывает на общую папку /home/stilicho/docker), удобнее не заводить отдельный репозиторий под каждый стек, а держать один общий монорепозиторий - с подпапкой на каждый стек внутри. Так все compose-файлы видны Komodo сразу через один Repo-ресурс, и не приходится плодить десятки отдельных записей.

.env-файлы в таком репозитории можно коммитить как есть, но обязательно делайте репозиторий приватным - иначе секреты (пароли БД, токены) окажутся доступны кому угодно, у кого есть ссылка на репозиторий.

Создаем Stack
#

  1. Resources → Stacks → Create Stack.
  2. Даем стеку имя (например, traefik, если это стек с реверс-прокси, или nginx-test для тестового примера).
  3. Server - на каком хосте (Periphery-агенте) разворачивать стек. Если у вас пока один хост - выбирать особо не из чего, но при добавлении второго хоста через отдельный Periphery-агент это поле определяет, куда именно уедет деплой.
  4. Select Repo - здесь либо выбираем ранее созданный Repo-ресурс, либо (если отдельный Repo-ресурс не заводили) настраиваем источник прямо тут же: Git Provider → Account → Repo (формат тот же - владелец/репозиторий, например stilicho/youtube) → Branch. Оба пути равнозначны, просто отдельный Repo-ресурс удобнее переиспользовать в Procedure, как показано ниже.
  5. Run Directory - путь до подпапки с compose-файлом внутри репозитория, например traefik, относительно корня репозитория.
  6. File Paths - имя самого compose-файла, которое будет подставлено в docker compose -f, относительно Run Directory. Если оставить пустым, Komodo подставит compose.yaml - если ваш файл называется иначе (например, привычнее многим docker-compose.yml), это обязательно нужно прописать явно в этом поле, иначе деплой упадет на шаге валидации с ошибкой вида Missing files: compose.yaml, даже если сам файл на месте, просто под другим именем.
  7. Сохраняем и жмем Deploy - Komodo должна поднять контейнер(ы) из вашего compose-файла.
Note

Там же, чуть ниже, есть поле Clone Path - по умолчанию Komodo клонирует репозиторий в $root_directory/stacks/<имя стека> (в нашем случае это будет /home/stilicho/docker/stacks/traefik), и в 99% случаев трогать это поле не нужно. Полезно просто знать, что оно означает - именно этот путь вы увидите в логе шага Clone Repo, если понадобится туда зайти руками и что-то проверить.

Warning

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.

  1. Resources → Procedures → Create Procedure.
  2. Даем процедуре имя, например deploy-on-push.
  3. Добавляем шаги по порядку (кнопка 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\.). Чтобы шаг подхватывал вообще любой изменившийся стек в репозитории - ставим туда просто *.
  4. Сохраняем Procedure.
Tip

Вариант “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.

  1. Открываем страницу Procedure deploy-on-push (или страницу конкретного Stack, если решили обойтись без Procedure) в Komodo - в разделе Webhooks там уже будет готовый URL, собирать вручную ничего не придется. Для Procedure это будет выглядеть так:

    https://komodo.stilicho.ru/listener/github/procedure/deploy-on-push/main
  2. В Forgejo идем в нужный репозиторий → Settings → Webhooks → Add Webhook → Forgejo (Forgejo поддерживает свой нативный формат webhook, отдельный от универсального Gitea-совместимого - выбирайте именно Forgejo, если он есть в списке; если нет, подойдет и Gitea - формат событий совместим и с тем, и с другим).

  3. Заполняем форму:

    • 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’ов для нашей задачи не нужны).
  4. Сохраняем webhook. Forgejo сразу предложит отправить тестовый Test Delivery - стоит им воспользоваться: если Komodo ответит 200 OK, значит секрет и URL настроены верно, и можно переходить к проверке на реальном коммите.

Warning

Если в ответ на 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.

Note

Если в имени вашей 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
Tip

Изменить файл можно и прямо в веб-интерфейсе 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 push

git 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 и откат в одну команду.

Self-Hosting - This article is part of a series.
Part : This Article

Related

GitLab, Gitea и Forgejo. Сравнение не для снобов - кому что подходит для homelab

·1221 слово·6 минут· loading · loading
Сравниваю GitLab CE, Gitea и Forgejo по лицензиям, системным требованиям, управлению проектом и CI/CD - и объясняю, почему совет «пользуйтесь только GitLab, остальное для необразованных» не имеет отношения к реальным инженерным аргументам.

Переезжаем с Portainer на Komodo: полная установка и настройка

··5397 слов·26 минут· loading · loading
Пошаговая инструкция по установке Komodo как альтернативы Portainer: docker-compose, MongoDB, bind mount вместо volumes, вход через Authentik (OIDC) и подключение дополнительных хостов через Periphery.