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

Forgejo + Komodo — превращаем Docker Compose в GitOps

·2179 слов·11 минут· loading · loading · · Черновик
Stilicho2011
Автор
Stilicho2011
Пишу о homelab, self-hosting, автоматизации и open-source решениях
Оглавление
83 - This article is part of a series.

Зачем это вообще нужно
#

После переезда со всех compose-файлов на Komodo осталась одна незакрытая проблема: сами файлы docker-compose.yml по-прежнему лежат простыми текстовыми файлами на диске, без истории изменений, без бэкапа “что было вчера до того, как мои шаловливые ручки решили что-то подправить”, без единого общего репо, где хранится актуальная версия конфигурации. Правишь конфиг прямо на проде (да, да, у нас же хоумлаберов тоже свой прод есть, правда!?) — и если что-то пошло не так, откатываться не то чтобы не на что, а просто ты уже и не помнишь с чего начинал, и где та самая еще рабочая версия до того как ты все решил улучшить, что привело к полной неработоспособности системы.

Решение — завести собственный git-сервер (Forgejo) и связать его с Komodo так, чтобы:

  • каждое изменение compose-файла фиксировалось в git;
  • git push автоматически подтягивался на сервер и передеплоивал только изменившиеся стэки;
  • всё это работало не только на основном хосте, но и на удалённых серверах (в моём случае — второй хост с Immich, второй хост с Nextcloud и т.д.).

Дальше — весь путь от установки Forgejo до рабочего GitOps-пайплайна, с граблями, на которые я наступил (кое-где повторно), чтобы вы не наступали.

Шаг 1. Устанавливаем Forgejo
#

Forgejo — форк Gitea, лёгкий self-hosted git-сервер. Ставится обычным compose-стэком. У меня уже есть рабочая база данных Postgres на отдельном LXC-контейнере (управляю ей через pgAdmin), поэтому Forgejo подключил к нему, а не поднимал ещё один Postgres-контейнер рядом. Ниже я описываю, как это я делал у себя. В твоем случае, если у тебя нет единой базы данных, а есть отдельные для каждого стэка, то просто добавишь в компоуз данные для бд из официальной документации.

База данных
#

В pgAdmin создаём роль и базу:

CREATE ROLE forgejo WITH LOGIN PASSWORD 'пароль';
CREATE DATABASE forgejo
  OWNER forgejo
  ENCODING 'UTF8'
  TEMPLATE template0;

Важный момент: если через GUI pgAdmin создать базу без явного указания TEMPLATE template0, можно получить ошибку new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII) — на некоторых серверах template1 исторически настроен в другой кодировке. Я, как обычно, учусь на своих ошибках и постоянно об этом забываю, но тебе то не так не надо?!

Docker-compose с метками для обратного прокси Traefik
#

sudo nano docker-compose.yaml
services: # Объявляем список сервисов, которые должен создать Docker Compose.
  server: # Имя сервиса внутри Compose. На него можно ссылаться как на "server".
    image: codeberg.org/forgejo/forgejo:14 # Используем образ Forgejo версии 14 из реестра Codeberg.
    container_name: forgejo # Явно задаём имя Docker-контейнера вместо автоматически сгенерированного Compose имени.
    environment: # Передаём переменные окружения внутрь контейнера Forgejo.
      - USER_UID=1000 # UID пользователя, от имени которого Forgejo работает с файлами в /data.
      - USER_GID=1000 # GID группы, от имени которой Forgejo работает с файлами в /data.
      - FORGEJO__database__DB_TYPE=postgres # Говорим Forgejo использовать PostgreSQL в качестве базы данных.
      - FORGEJO__database__HOST=айпи_хоста:5432 # Адрес PostgreSQL-сервера и его порт. Вместо "айпи_хоста" указывается реальный IP-адрес.
      - FORGEJO__database__NAME=forgejo # Имя базы данных PostgreSQL, созданной для Forgejo.
      - FORGEJO__database__USER=forgejo # Имя пользователя PostgreSQL, от имени которого Forgejo подключается к базе.
      - FORGEJO__database__PASSWD=${FORGEJO_DB_PASSWORD} # Пароль PostgreSQL берётся из переменной FORGEJO_DB_PASSWORD, определённой вне этого compose-файла.
    restart: always # Docker будет автоматически перезапускать контейнер при его остановке, в том числе после перезагрузки хоста.
    networks: # Подключаем контейнер к указанным Docker-сетям.
      - proxy # Подключаем Forgejo к внешней сети proxy, через которую к нему обращается Traefik.
    volumes: # Объявляем постоянные хранилища и bind-mounts контейнера.
      - ./forgejo:/data # Каталог ./forgejo рядом с compose-файлом монтируется в /data контейнера; здесь хранятся данные Forgejo.
      - /etc/localtime:/etc/localtime:ro # Передаём контейнеру системное локальное время хоста; режим ro запрещает запись в файл.
    ports: # Публикуем порты контейнера непосредственно на Docker-хосте.
      - '3000:3000' # Порт 3000 хоста перенаправляется на HTTP-порт Forgejo 3000.
      - '222:22' # Порт 222 хоста перенаправляется на SSH-порт Forgejo 22.
    labels: # Docker labels используются Traefik для автоматического обнаружения и настройки маршрутизации.
      - "traefik.enable=true" # Разрешаем Traefik обрабатывать этот контейнер.
      - "traefik.http.routers.forgejo.entrypoints=web" # Создаём HTTP-роутер Forgejo на entryPoint web, обычно это порт 80.
      - "traefik.http.routers.forgejo.rule=Host(`forgejo.example.com`)" # Роутер будет срабатывать для запросов к указанному доменному имени.
      - "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-роутер на entryPoint websecure, обычно это порт 443.
      - "traefik.http.routers.forgejo-secure.rule=Host(`forgejo.example.com`)" # HTTPS-роутер также обслуживает указанный домен.
      - "traefik.http.routers.forgejo-secure.tls=true" # Включаем TLS для HTTPS-роутера.
      - "traefik.http.routers.forgejo-secure.service=forgejo" # Указываем HTTPS-роутеру использовать сервис Traefik с именем forgejo.
      - "traefik.http.services.forgejo.loadbalancer.server.port=3000" # Traefik должен отправлять запросы из сети proxy на порт 3000 контейнера Forgejo.
      - "traefik.docker.network=proxy" # Явно указываем Traefik использовать Docker-сеть proxy для подключения к контейнеру.
    security_opt: # Дополнительные параметры безопасности контейнера.
      - no-new-privileges:true # Запрещаем процессам контейнера получать дополнительные привилегии через механизмы повышения прав.

networks: # Объявляем Docker-сети, используемые сервисами Compose.
  proxy: # Описываем сеть с именем proxy.
    external: true # Говорим Compose, что сеть уже создана отдельно и создавать её заново не нужно.
sudo nano .env
FORGEJO_DB_PASSWORD=пароль

Описание инфрастуктуры
#

Схема работы этого Compose-файла выглядит так:


                         ┌──────────────────────┐
                         │       Traefik        │
                         │        :80/:443      │
                         └──────────┬───────────┘
                                    │
                              Docker network
                                  "proxy"
                                    │
                                    ▼
                         ┌──────────────────────┐
                         │       Forgejo        │
                         │       :3000          │
                         │       :22 (SSH)      │
                         └──────────┬───────────┘
                                    │
                          ./forgejo:/data
                                    │
                                    ▼
                         ┌──────────────────────┐
                         │ Данные Forgejo       │
                         │ на Docker-хосте      │
                         └──────────────────────┘
                                    │
                                    │ PostgreSQL
                                    ▼
                         ┌──────────────────────┐
                         │   PostgreSQL         │
                         │   <IP>:5432          │
                         │   database: forgejo  │
                         └──────────────────────┘

Два разных способа доступа к Forgejo
#

HTTP-доступ пользователей идёт через Traefik:

Браузер
   │
   │ https://forgejo.example.com
   ▼
Traefik :443
   │
   │ Docker network "proxy"
   ▼
Forgejo :3000

SSH-доступ Git работает иначе:

Git client
   │
   │ SSH :222
   ▼
Docker host :222
   │
   ▼
Forgejo container :22

Поэтому 3000:3000 и 222:22 здесь выполняют разные задачи: первый порт нужен для HTTP-доступа к Forgejo, второй — для Git по SSH.

Несколько важных замечаний
#

ports и Traefik
#

Если HTTP-доступ к Forgejo полностью организован через Traefik и сам Forgejo не должен быть доступен напрямую с сети, публикация:

ports:
  - '3000:3000'

может быть вообще не нужна. Traefik находится в той же Docker-сети proxy и может обращаться к forgejo:3000 напрямую.

При этом 222:22 нужен, если планируется использовать SSH-доступ к Git с хоста или из внешней сети.

Пароль PostgreSQL
#

Строка:

- FORGEJO__database__PASSWD=${FORGEJO_DB_PASSWORD}

не содержит пароль непосредственно в compose-файле. Docker Compose подставляет значение переменной FORGEJO_DB_PASSWORD из окружения или .env/другого используемого механизма передачи переменных.

Это предпочтительнее, чем хранить пароль непосредственно в YAML.

Внешняя сеть proxy
#

Строка:

external: true

означает, что сеть должна существовать до запуска Compose. Например:

docker network create proxy

Если сеть не создана заранее, docker compose up -d завершится ошибкой.

Следующие команды
#

docker compose up -d — и по нашему доменному имени https://forgejo.example.com открывается install wizard. Там всё стандартно, кроме одного поля, которое легко пропустить: Base URL. По умолчанию там http://localhost:3000/ — если не поправить на реальный домен, сломаются HTTPS-ссылки для клонирования и уведомления.

OpenID Connect сразу включил (пригодится позже для authentik), а self-registration оставил закрытой. Нам это не к чему, потому что у нас личный закрытый сервер.

Шаг 2. Монорепозиторий вместо репозитория на каждый стэк
#

Вариантов организации git было два: отдельный репозиторий на каждый стэк/VM/LXC или один общий репозиторий со всеми compose-файлами. Выбрал второй — проще администрировать, один Repo-ресурс в Komodo, один вебхук, одна Procedure на все стэки разом. Безусловно есть определенное неудобство о чем ниже, но с ним можно жить. Да и всегда можно будет разнести по разным репо, что мне кажется проще, чем сливать разные репозитории в один.

Создаю в Forgejo пустой приватный репозиторий (без README и .gitignore через веб-интерфейс — они появятся из первого коммита) и превращаю в git-репозиторий прямо ту папку, где, уже который год (у меня), лежат все compose-файлы:

cd /home/твой_путь/docker
git init
git branch -M main
git remote add origin https://forgejo.example.com/user/docker-stacks.git

Шаг 3. .gitignore — самая долгая часть
#

Вот тут у меня началось самое интересное. Ну может и не самое, подумаешь с бубном потанцевал пару тройку часов. Первая наивная попытка git add -A в директории, где год копились данные 30+ контейнеров, выдаёт список из полутысячи с лишним файлов (не шутка или гипербола). Разбирать его вручную - не вариант, поэтому пошёл по пути “сначала находим самое тяжёлое и самое опасное, потом причёсываем остальное”.

Что искать в первую очередь — размер:

du -sh /home/твой_путь/docker/*/* 2>/dev/null | sort -rh | head -30

Так определил гигабайты медиатек *arr-стэка (в основном это обложки от Lidarr и его же бекапы), базы уведомлений, кэши обложек аудиокниг — всё, что явно данные, а не конфигурация.

Что искать во вторую — секреты. Это оказалось важнее размера. В git add -A без разбора чуть не попали:

  • traefik/data/acme.json — приватный ключ аккаунта Let’s Encrypt и все выданные сертификаты (зачем нам это в нашем репо, правльно?);
  • vaultwarden/data/ — база менеджера паролей (это вообще самое важное);
  • собственная папка данных Forgejo (forgejo/forgejo/) — сессии, JWT-ключ, и что забавно, сам git-репозиторий, который мы же и создаём, лежащий внутри самого себя;
  • komodo/keys/ и komodo/mongo/data/ — ключи Periphery и база самой Komodo;
  • komodo/backups/ — а вот это уже было по-настоящему неприятно: автоматические ежедневные бэкапы Komodo включают экспорт токенов git-провайдеров и API-ключей в гзипованном виде. Эта папка попала в один из ранних коммитов раньше, чем я это заметил (правильно наверно сказать подумал) — узнал только разобрав тело вебхука, который Forgejo прислала на push. Хорошо, что репозиторий приватный и путь до этого коммита короткий, но токены на всякий случай стоит перевыпустить. Мало ли что?

Итоговый .gitignore получился длинным — где-то по три-пять строк на каждый стэк с базой данных или кэшем. Общий принцип, который я для себя вывел:

Tip

В git должно попадать только то, что ты написал руками. Всё, что сгенерировал контейнер сам — базы, ключи, кэши, логи, сессии — не конфигурация, а данные. Данные версионировать бессмысленно (они меняются каждую секунду) и вредно (там могут быть секреты).

Рабочий цикл проверки был такой:

git reset
git add -A
git status   # смотрим, что попало в staging
# находим лишнее → дописываем .gitignore → повторяем

Отдельно всплыла проблема прав: часть файлов создана контейнерами от нестандартного UID, и обычный пользователь их даже прочитать не может — git add падает с Отказано в доступе. Такие пути тоже пришлось добавлять в .gitignore по мере появления ошибок.

Шаг 4. Первый коммит и push
#

git config --global user.name "твое имя в репо"
git config --global user.email "you@example.com"

git commit -m "Initial import of docker stacks"
git remote set-url origin https://user:TOKEN@forgejo.example.com/user/docker-stacks.git
git push -u origin main
git remote set-url origin https://forgejo.example.com/user/docker-stacks.git

Токен для push — Personal Access Token из Forgejo (Settings → Applications → Generate New Token, права repository: Read and Write).

Шаг 5. Связываем с Komodo
#

Здесь у Komodo есть два принципиально разных подхода:

  1. Перевести каждый Stack на git-режим — прописать в конфиге каждого стэка repo/branch/run_directory. Минус: Komodo клонирует репозиторий в свою собственную директорию, и все относительные bind-mount пути (./data:/data в compose-файлах) окажутся уже не там, где раньше — есть риск переехавших данных.
  2. Завести отдельный ресурс Repo, который просто следит за репозиторием и умеет делать git pull — при этом указать ему путь клонирования = та же директория, где уже лежат стэки. Тогда git pull просто обновляет файлы на месте, а конфигурация Stack-ресурсов (files on server, абсолютные пути) вообще не меняется.

Выбрал второй — он безопаснее для уже работающей инфраструктуры.

Настройка:

  1. Settings → Git Accounts → добавить аккаунт для forgejo.example.com с токеном.
  2. Repos → New Repo → указать сервер, git-аккаунт, репозиторий, ветку main и Path = та самая директория со стэками.
  3. Procedures → New Procedure → Stage 1: Pull Repo → Stage 2: Batch Deploy Stack If Changed с таргетом * (маска “все стэки”).
  4. На странице Procedure — вкладка Webhooks, копируем готовый URL вида https://komodo.example.com/listener/github/procedure/<id>/main.
  5. В Forgejo: репозиторий → Settings → Webhooks → Add Webhook → тип Gitea (полностью совместим с “Github” auth style в Komodo) → вставляем URL, секрет, событие Push.

Шаг 6. Проверка
#

echo "# test" >> some-stack/docker-compose.yaml
git add . && git commit -m "test webhook" && git push

Дальше смотрим тело доставки в Recent Deliveries на стороне Forgejo (там виден весь payload — какие файлы добавлены/изменены) и состояние Repo/Stack-ресурсов в Komodo — у обновлённого стэка должен смениться commit hash и пройти передеплой.

Шаг 7. Масштабируем на второй хост
#

У меня есть отдельный сервер с Immich, подключённый к Komodo как удалённый Periphery-агент. Заводить для него отдельный репозиторий не стал — тот же монорепозиторий, стэки этого хоста просто легли туда ещё одной подпапкой.

Единственная тонкость — при первом git checkout на втором хосте, где уже есть локальные файлы (в моём случае — свежесозданный .gitignore), git отказывается переключаться на ветку из-за риска быть перезаписанным (“would be overwritten by checkout”). Решается просто: убрать конфликтующий файл, он всё равно придёт из репозитория при checkout, а специфичные для этого хоста строки (в моём случае — immich/model-cache/, immich/postgres/, keys/) дописываются в общий .gitignore уже после переключения на ветку.

Дальше — тот же рецепт: отдельный Repo-ресурс в Komodo с путём на этом сервере, и второй Pull Repo в том же Stage 1 существующей Procedure. Batch Deploy с маской * подхватывает стэки любого сервера сам, никаких изменений в Stage 2 не потребовалось.

Итог
#

  • Все compose-файлы под git, с историей изменений;
  • git push из VS Code (а на Винде я пользуюсь именно им) автоматически прокатывается на все нужные серверы;
  • секреты и данные контейнеров осознанно исключены — в репозитории только то, что действительно нужно для восстановления инфраструктуры с нуля;
  • расширяется на новые хосты без переделки схемы — просто ещё один Repo-ресурс в Komodo и ещё одна строка в Stage 1.
83 - This article is part of a series.

Related

Черновик

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

·3181 слово·15 минут· loading · loading
Зачем переходить с Portainer на Komodo # Portainer долго был стандартом де-факто для управления Docker через веб-интерфейс, но у него есть ограничения: нет полноценного CI/CD-воркфлоу, слабая работа с git-репозиториями, устаревающий UI. Komodo — более молодой инструмент (исходники на GitHub), который закрывает эти пробелы: он умеет деплоить стеки прямо из git, поддерживает распределённое управление несколькими хостами через агент Periphery, и предлагает более современный подход к CI/CD внутри домашней инфраструктуры. Собственно говоря, на эти вещи разработчики и делали упор судя по всему. Полный список возможностей — в официальном описании “What is Komodo”.
Черновик

Arcane — современный менеджер Docker: обзор, установка и настройка

·1503 слов·8 минут· loading · loading
Обзор Arcane — молодого, но быстро растущего веб-интерфейса для Docker — и пошаговая инструкция по установке, настройке секретов, подключению существующих compose-проектов и базовой защите сокета.