Навигация
Установка и настройка GitRiver
Установка GitRiver: быстрый запуск через Docker Compose, пакеты deb и rpm, чарт Helm, обратный посредник, обновление и проверка поставки
Ставится тремя способами: образ Docker (compose), системный пакет deb/rpm, Helm-чарт для Kubernetes. Возможности одинаковы во всех трёх — различаются только способ доставки и место конфигурации:
| Способ | Конфигурация | Обновление | Кому |
|---|---|---|---|
| Docker Compose | ./data/gitriver/gitriver.toml, переменные в compose |
docker compose pull && up -d |
стенд, небольшая установка, знакомство |
| Пакет deb/rpm | /etc/gitriver/gitriver.toml, дополнение службы systemd |
apt upgrade / dnf upgrade |
машина дистрибутива, эксплуатация без Docker |
| Helm-чарт | values.yaml |
helm upgrade |
кластер Kubernetes |
Сборочные машины ставятся отдельно и своим пакетом — см. «Установка внешнего исполнителя».
Подлинность поставки
К каждому выпуску приложены SHA256SUMS со суммами всех файлов и SHA256SUMS.asc
— подпись сопровождающего. Неподписанный выпуск не публикуется: без подписи
отличить поставку издателя от подменённой по дороге нечем.
# ключ издателя приложен к выпуску; отпечаток сверьте с опубликованным на сайте
gpg --import gitriver-release-key.asc
gpg --show-keys --with-fingerprint gitriver-release-key.asc # он же — в gitriver-release-key.fingerprint
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum -c SHA256SUMS
Отпечаток лежит рядом отдельной строкой — gitriver-release-key.fingerprint, —
и входит в подписанный перечень сумм. Сверять его всё равно следует с
опубликованным на сайте издателя: файл рядом с ключом подтверждает только то,
что они друг другу соответствуют.
Образы отдельной подписи не имеют — их дайджесты входят в тот же подписанный
перечень: image-digest.txt для сервера, runner-image-digest.txt для
внешнего исполнителя. Проверка после загрузки образа:
docker pull gitriver/gitriver:1.1.0
docker image inspect --format '{{index .RepoDigests 0}}' gitriver/gitriver:1.1.0
cat image-digest.txt # должно совпасть
Сверяется дайджест списка манифестов — то, что отдаёт реестр по тегу. При
классическом хранилище Docker его покажет и docker image inspect
(RepoDigests); при хранилище containerd там может оказаться дайджест одной
архитектуры — тогда берите его у реестра:
docker buildx imagetools inspect gitriver/gitriver:1.1.0.
Проверяйте подпись до установки, а не после: сумма, взятая оттуда же, откуда и файл, ничего не подтверждает.
Требования
- PostgreSQL 15+ с доступным расширением
pgcrypto(docker-compose.prod.ymlиз поставки поднимает 18; на RHEL-семействе расширение — в пакетеpostgresql-contrib) - 2+ CPU, 4+ GB RAM (рекомендуется)
- Docker 20.10+ с BuildKit — для установки образом и для встроенного исполнителя CI (доступ к сокету демона)
Со встроенным исполнителем сервер обслуживает кеш сборки на всей машине. Раз в сутки (и один раз при запуске) он оставляет не более 10 ГБ кеша у КАЖДОГО сборщика BuildKit на хосте — включая сборщики, заведённые не им и не для CI. Иначе тома сборщиков растут без предела. Если на этой машине собирается что-то ещё, учитывайте это при выборе хоста.
Docker нужен серверу только для встроенного исполнителя CI. Если задания выполняют внешние исполнители, сервер обходится без него — см. «Лёгкая установка без Docker».
Быстрый старт (Docker Compose)
Понадобятся два файла из выпуска — docker-compose.prod.yml и
gitriver.env.example:
mkdir gitriver && cd gitriver
# положите сюда docker-compose.prod.yml и gitriver.env.example
cp gitriver.env.example .env
${EDITOR:-nano} .env # GITRIVER_DB_PASS — любой пароль, GITRIVER_BASE_URL=http://localhost:3000
docker compose -f docker-compose.prod.yml up -d
Сервер доступен на http://localhost:3000. При первом входе открывается
мастер настройки — он создаст администратора.
Так знакомятся с продуктом на своей машине. Для установки, доступной из сети, берите рабочую установку: мастер настройки не защищён ничем, и открытый в сеть он отдаёт установку первому, кто до него дошёл.
Установка в прод
1. Подготовка
mkdir -p /opt/gitriver/data/{gitriver,postgres,registry}
cd /opt/gitriver
Положите сюда docker-compose.prod.yml и образец окружения
gitriver.env.example — оба приложены к выпуску.
2. Файл .env
Все значения рабочей установки задаются в .env рядом с compose-файлом —
сам файл править не нужно:
cp gitriver.env.example .env
${EDITOR:-nano} .env # пароль базы, внешний адрес
Обязательны GITRIVER_DB_HOST, GITRIVER_DB_USER, GITRIVER_DB_PASS,
GITRIVER_DB_NAME и GITRIVER_BASE_URL; остальное описано в самом образце.
Запуск без .env останавливается с перечислением недостающих переменных, и
это защита, а не помеха. Сервер без подключения к базе открывает мастер
первичной настройки, а порт 3000 проброшен наружу: кто первым откроет страницу,
тот и задаст базу и создаст администратора. То же предупреждение — для установки
из пакета, см. «Настройка».
3. Запуск
docker compose -f docker-compose.prod.yml up -d
Проверить, что окружение прочитано и подставлено, можно не поднимая службы:
docker compose -f docker-compose.prod.yml config
Из поставки приходит образ издателя с тегом версии. Своё зеркало —
GITRIVER_IMAGE в .env. Тег закреплён за версией (не latest): установка
меняется только тогда, когда вы сами меняете тег.
Служба registry в этом файле — реестр образов для заданий CI. Пароля у него нет,
а порт 5000 публикуется на все адреса, потому что образы забирают контейнеры
заданий: закройте этот порт снаружи правилами сетевого экрана. Если 5000 на
машине уже занят, задайте другой порт хоста переменной GITRIVER_REGISTRY_PORT
в .env — внутри контейнера порт остаётся 5000, и адрес для заданий задаётся
отдельно (GITRIVER_CI_REGISTRY, ниже).
Задания с docker push в реестр инстанса требуют GITRIVER_CI_REGISTRY в
.env. Без него адрес реестра (CI_REGISTRY в заданиях) строится из
GITRIVER_BASE_URL, а там порт сервера — реестра на нём нет: docker login
отказывает, а docker push проваливает задание. Задайте адресом, который
достанет демон Docker хоста: и исполнитель, и контейнеры заданий ходят к нему
через его сокет, поэтому alias контейнеров (host.docker.internal) не годится
— внешний адрес хоста с портом 5000, например 192.0.2.1:5000. Реестр из этого
файла TLS не имеет и отдаёт только http, а демону нужен явный список: впишите
адрес реестра в insecure-registries в конфигурации Docker.
4. Обратный прокси (nginx)
server {
listen 443 ssl http2;
server_name git.example.com;
ssl_certificate /etc/ssl/certs/git.example.com.pem;
ssl_certificate_key /etc/ssl/private/git.example.com.key;
client_max_body_size 512m; # Для LFS и больших push
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE (CI журналы, события pipeline)
proxy_buffering off;
proxy_read_timeout 3600s;
}
}
Установка из пакета (deb/rpm)
Пакет ставит бинарник (веб-интерфейс встроен в него), unit-файл systemd,
заводит пользователя gitriver и каталоги данных. Docker при этом не требуется — если
задания CI выполняют внешние исполнители (см.
«Лёгкая установка без Docker»).
Пакету нужна glibc 2.34 или новее: он ставится на RHEL, Rocky и AlmaLinux 9+, Debian 12+, Ubuntu 22.04+, Astra Linux 1.8+ и другие дистрибутивы не старше их. Для более старых остаётся установка образом.
Архитектуры две — x86-64 и arm64: к выпуску приложены оба набора пакетов
(_amd64.deb / .x86_64.rpm и _arm64.deb / .aarch64.rpm), а образы
собраны списком манифестов, и docker pull берёт из них подходящий сам.
1. Установка
# Debian, Ubuntu, Astra
sudo apt install ./gitriver_1.1.0-1_amd64.deb
# RHEL, Rocky, AlmaLinux, РЕД ОС
sudo dnf install ./gitriver-1.1.0-1.x86_64.rpm
Пакеты приложены к выпуску: скачайте их со страницы выпуска вместе с файлом
SHA256SUMS и его подписью и сверьте перед установкой — как это сделать,
описано в разделе «Подлинность поставки».
2. База данных
RHEL, Rocky, AlmaLinux, РЕД ОС: сначала включите поток PostgreSQL 15 или
новее. Поток по умолчанию в этих системах — 13, а пакет рекомендует
postgresql-server >= 15: невыполнимую рекомендацию dnf молча пропускает, и
база не поставится вовсе.
sudo dnf module enable -y postgresql:16
sudo dnf install -y postgresql-server postgresql-contrib # contrib — расширение pgcrypto
sudo postgresql-setup --initdb
# вход по паролю: по умолчанию здесь ident, и сервер получит «Ident authentication failed»
sudo sed -i -E 's/^(host\s.*)\bident$/\1scram-sha-256/' /var/lib/pgsql/data/pg_hba.conf
sudo systemctl enable --now postgresql
На старой базе сервер работать не откажется, но при запуске предупредит: версии ниже 15 не проверяются.
sudo -u postgres createuser gitriver --pwprompt
sudo -u postgres createdb gitriver --owner gitriver
3. Настройка
Строка подключения вписывается в /etc/gitriver/gitriver.toml:
database_url = "postgres://gitriver:пароль@localhost:5432/gitriver"
base_url = "https://git.example.com"
Оставить database_url пустым тоже можно: тогда сервер поднимет мастер
первичной настройки и запишет строку подключения сам. Мастер доступен любому,
кто дотянется до порта, — на машине с внешним адресом закройте порт до конца
настройки.
Что не должно попадать в файл, который правит мастер (пароли, токены, переопределения окружения), задаётся дополнением службы systemd:
sudo systemctl edit gitriver
[Service]
Environment=GITRIVER_BASE_URL=https://git.example.com
Environment=GITRIVER_METRICS_TOKEN=длинная-случайная-строка
Переменные окружения перекрывают TOML.
4. Запуск
sudo systemctl enable --now gitriver
sudo systemctl status gitriver
journalctl -u gitriver -f
Служба не запускается сразу при установке пакета намеренно: до того, как задана база или закрыт доступ к порту, открывать мастер настройки в сеть не следует.
Что и где лежит
| Путь | Что это |
|---|---|
/usr/bin/gitriver |
сервер с встроенным веб-интерфейсом и консольные команды (run, backup, restore, serv, notices, init-config) |
/etc/gitriver/gitriver.toml |
настройки; при обновлении пакета не перезаписывается |
/etc/gitriver/.jwt_secret |
ключ подписи токенов, создаётся при установке |
/var/lib/gitriver/ |
репозитории, данные CI, Pages, реестр |
/usr/lib/systemd/system/gitriver.service |
unit-файл |
/usr/lib/systemd/system/gitriver.slice |
срез ресурсов CI (пределы ему задаются отдельно, см. «Пределы ресурсов заданий CI») |
/usr/share/doc/gitriver/ |
полный пример конфигурации, документация, лицензионное соглашение и перечень сторонних лицензий |
Каталог /etc/gitriver открыт службе и на запись — мастер настройки вписывает
в файл строку подключения. Ключи, которые сервер создаёт сам без явного пути
(подпись коммитов, ключ узла SSH), по умолчанию лежат рядом с файлом настроек,
в /etc/gitriver. Ключ подписи токенов создаётся при установке пакета и
переживает обновления; его смена закрыла бы сеансы всем пользователям.
Лицензии
Условия на сам GitRiver — в /usr/share/doc/gitriver/LICENSE.ru.md (и
LICENSE.en.md). Сторонний код с открытым исходным текстом, входящий в
поставку, перечислен вместе с условиями в
/usr/share/doc/gitriver/THIRD-PARTY-NOTICES.md: около пятисот библиотек
Rust (crates.io) и почти столько же пакетов npm с текстами их лицензий. Перечень
соответствует составу именно этой поставки.
Тот же файл печатает сам сервер — gitriver notices — и показывает интерфейс
на странице /legal, вкладка «Сторонние компоненты»: условия читаются и в
закрытом контуре, без выхода в интернет. В образе контейнера файл лежит там же:
/usr/share/doc/gitriver/THIRD-PARTY-NOTICES.md.
SSH
Встроенный SSH-сервер включается секцией [ssh_server] в конфиге. Unit-файл
разрешает службе привязку к портам ниже 1024 (CAP_NET_BIND_SERVICE), поэтому
занять 22 она может — но обычно он занят системным sshd, и тогда берут свой
порт:
ssh_port = 2222
[ssh_server]
listen = "0.0.0.0:2222"
host_key_path = "/var/lib/gitriver/ssh_host_ed25519_key"
Одновременно сервер обслуживает не более 256 соединений SSH. Соединение сверх предела закрывается сразу, клиент видит обрыв и повторяет; отказ пишется в журнал строкой «встроенный SSH: соединение отклонено — предел одновременных исчерпан».
Вариант с системным sshd: ключи пользователей сервер поддерживает сам, домашний
каталог учётной записи службы — /var/lib/gitriver:
authorized_keys_path = "/var/lib/gitriver/.ssh/authorized_keys"
Пределы ресурсов заданий CI
Строка Delegate=cpu pids memory в unit-файле уже стоит и нужна: без неё
задания без image: выполняются без пределов памяти и числа процессов. Там же
стоит Slice=gitriver.slice: срез, в котором считается весь расход CI. Предел ему
задаётся отдельно — он не берётся ни из пакета, ни из gitriver.toml:
systemctl set-property gitriver.slice CPUQuota=600% MemoryMax=12G
Подробнее — «Пределы ресурсов задания».
Обновление
sudo apt install ./gitriver_НОВАЯ-ВЕРСИЯ_amd64.deb # или dnf upgrade
Пакет перезапускает службу сам, если она была запущена; схему базы сервер обновляет сам при старте. Настройки, данные и ключ подписи токенов остаются на месте.
Удаление
apt remove / dnf remove останавливает службу и снимает файлы поставки.
Репозитории в /var/lib/gitriver и база данных не трогаются ни при удалении,
ни при apt purge — purge убирает только настройки и созданный установкой ключ
подписи токенов. Снос данных остаётся отдельным решением администратора.
dnf remove понятия purge не знает: правленый конфиг он сохраняет как
/etc/gitriver/gitriver.toml.rpmsave, а ключ подписи токенов остаётся на месте —
после повторной установки выданные токены продолжают приниматься.
Установка внешнего исполнителя (deb/rpm)
Внешний исполнитель — отдельный пакет gitriver-runner и ставится на другие
машины: на те, где выполняются сборки. Серверу он не нужен, а сборочной машине
не нужен сервер, поэтому и пакеты разные. На одной машине они уживаются: пути,
учётные записи и службы у них не пересекаются.
Тот же выпуск даёт и образ исполнителя — тем же репозиторием реестра, что и
образ сервера, отдельным тегом (<образ сервера>:<версия>-runner); им
поднимается исполнитель в контейнере и им же пользуется
автомасштабирование в Kubernetes.
1. Установка
# Debian, Ubuntu, Astra
sudo apt install ./gitriver-runner_1.1.0-1_amd64.deb
# RHEL, Rocky, AlmaLinux, РЕД ОС
sudo dnf install ./gitriver-runner-1.1.0-1.x86_64.rpm
Для arm64 — файлы gitriver-runner_1.1.0-1_arm64.deb и
gitriver-runner-1.1.0-1.aarch64.rpm из того же выпуска.
Пакеты исполнителя приложены к выпуску рядом с серверными и проверяются тем же
файлом SHA256SUMS.
Git, Git LFS и клиент SSH пакет ставит зависимостями. Docker не обязателен и
ставится отдельно, из любого источника (docker.io, docker-ce,
moby-engine): он нужен только заданиям с image:.
Служба при установке не запускается: без адреса сервера и токена ей нечего делать. Что делать дальше, пакет напоминает в конце установки — это шаги 2–5 ниже.
2. Токен исполнителя
Исполнитель заводится на сервере — «Администрирование» → «Исполнители» →
«Добавить». Токен
(grr_…) показывается один раз; там же задаются метки, по которым задания
попадут именно на эту машину (runs-on:), и область — сервер, группа или
репозиторий.
3. Настройка
Адрес сервера и токен вписываются в /etc/gitriver-runner/runner.env:
GITRIVER_URL=https://git.example.com
GITRIVER_RUNNER_TOKEN=grr_...
Адрес указывайте с https://. По http:// исполнитель работает, но
предупреждает при старте: токен и секреты заданий пойдут по сети открытым
текстом.
Остальные параметры в том же файле закомментированы вместе с пояснениями и
умолчаниями (рабочий каталог, интервал опроса сервера, пределы артефактов,
сроки операций git, образ для уборки рабочих копий, срез ресурсов); подробнее —
в руководстве администратора.
Файл читает служба, и он закрыт остальным (0640, группа gitriver-runner):
токен открывает очередь заданий, а с ней и секреты сборок. Не делайте файл
доступным для чтения всем.
Правка файла вступает в силу после перезапуска службы:
sudo systemctl restart gitriver-runner.
4. Доступ к Docker
Задания с image: исполнитель выполняет в контейнерах и обращается к демону
Docker на своей машине. Пакет доступа не выдаёт: членство в группе docker
равносильно root на машине, и это решение администратора, а не установки.
sudo systemctl edit gitriver-runner
[Service]
SupplementaryGroups=docker
Если служба уже запущена, перезапустите её: sudo systemctl restart gitriver-runner. Без этого доступа выполняются только задания без image: —
прямо на машине, правами учётной записи gitriver-runner.
5. Запуск
sudo systemctl enable --now gitriver-runner
journalctl -u gitriver-runner -f
В журнале при старте видно, к какому серверу исполнитель подключился и какой каталог занял. Появился он и в админке сервера («Исполнители») — со своими метками и временем последнего обращения.
Если исполнитель в админке не появился, причина — в журнале службы: чаще всего
это неверный адрес сервера, сервер недоступен с этой машины или токен неверен
(например, исполнитель удалён на сервере). Исправьте runner.env и
перезапустите службу.
Что и где лежит
| Путь | Что это |
|---|---|
/usr/bin/gitriver-runner |
исполнитель |
/etc/gitriver-runner/runner.env |
адрес сервера, токен и параметры; при обновлении пакета не перезаписывается |
/var/lib/gitriver-runner/ |
рабочие копии заданий, архивы артефактов и кеша |
/usr/lib/systemd/system/gitriver-runner.service |
unit-файл |
/usr/share/doc/gitriver-runner/ |
документация, лицензионное соглашение и перечень сторонних лицензий |
Рабочий каталог заводит systemd (StateDirectory) и отдаёт его исполнителю
целиком, правами 0700: внутри лежат рабочие копии заданий вместе с их
учётными данными. Каталог, принадлежащий другому пользователю, исполнитель
отвергает и не стартует — см. «Рабочий каталог исполнителя».
Исполнитель в контейнере
docker run -d --name gitriver-runner \
-e GITRIVER_URL=https://git.example.com \
-e GITRIVER_RUNNER_TOKEN=grr_... \
-v /var/run/docker.sock:/var/run/docker.sock \
gitriver/gitriver:1.1.0-runner
Сокет демона пробрасывается ради заданий с image: — контейнеры задания
поднимаются на хосте, рядом с самим исполнителем, а не внутри него. Рабочий
каталог, проброшенный с хоста (-v), должен принадлежать пользователю, от
которого исполнитель работает внутри контейнера (root), иначе он не стартует.
Обновление и удаление
sudo apt install ./gitriver-runner_НОВАЯ-ВЕРСИЯ_amd64.deb # или dnf upgrade
Пакет перезапускает службу сам, если она была запущена. Настройки и рабочий
каталог остаются на месте; apt remove останавливает службу и снимает файлы
поставки, а purge убирает ещё и настройки. Рабочий каталог не трогает ни то,
ни другое: в нём могут лежать рабочие копии незаконченных заданий.
dnf remove понятия purge не знает: правленый runner.env он сохраняет как
/etc/gitriver-runner/runner.env.rpmsave — токен в нём остаётся на месте, и
после повторной установки он принимается снова.
Установка в Kubernetes (Helm)
Чарт ставит Deployment, Service, при желании Ingress, постоянный том под репозитории и Secret с настройками. База данных по умолчанию внешняя.
Установка
Чарт приложен к выпуску архивом gitriver-1.1.0.tgz:
helm install gitriver gitriver-1.1.0.tgz \
--namespace gitriver --create-namespace \
--set database.host=postgres.internal \
--set database.password=<ПАРОЛЬ> \
--set config.baseUrl=https://git.example.com \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set ingress.hosts[0].host=git.example.com
Те же значения удобнее держать в своём файле:
helm install gitriver gitriver-1.1.0.tgz -f values.yaml
Пароль лучше держать в своём Secret, а не в командной строке:
kubectl -n gitriver create secret generic gitriver-db --from-literal=password=<ПАРОЛЬ>
helm install gitriver gitriver-1.1.0.tgz --set database.existingSecret=gitriver-db
Ключ подписи токенов
Ключом подписываются выданные токены, и от него же зависит часть сохранённых секретов: одноразовые коды двухфакторной проверки, секреты приложений OAuth, токены посредника реестра. Сменился ключ — все вышли из системы, а эти секреты перестали читаться (подробнее — «Ошибки токенов после перезапуска»).
Чарт передаёт ключ серверу переменной GITRIVER_JWT_SECRET из Secret; в
gitriver.toml он не пишется. Откуда берётся значение:
config.existingJwtSecret.name— из готового Secret (ключ в нём по умолчаниюjwt-secret). Единственный путь, пригодный для GitOps;config.jwtSecret— из values как есть (попадёт в Secret<выпуск>-gitriver-signing-keyоткрытым текстом values);- ничего не задано — чарт генерирует ключ при первой установке, кладёт в Secret
<выпуск>-gitriver-signing-keyи приhelm upgradeпереносит прежнее значение. Secret помеченhelm.sh/resource-policy: keep, как и тома данных: он переживаетhelm uninstall, и повторная установка под тем же именем подхватывает его.
Имена объектов чарта строятся так: <выпуск>-gitriver, но если имя выпуска уже
содержит gitriver, приставка не повторяется. При установке как в примерах
этого раздела (helm install gitriver …) Secret называется
gitriver-signing-key, а не gitriver-gitriver-signing-key; то же касается
томов ниже.
Задать оба сразу нельзя: установка остановится с объяснением.
Третий путь под GitOps не работает: Argo CD и Flux рендерят чарт без
кластера (helm template), прежнего ключа там не видно, и каждая синхронизация
выпускала бы новый. Такой рендер чарт останавливает с объяснением. Под GitOps
создайте Secret один раз и укажите его:
kubectl -n gitriver create secret generic gitriver-jwt \
--from-literal=jwt-secret="$(openssl rand -base64 48)"
helm install gitriver gitriver-1.1.0.tgz --set config.existingJwtSecret.name=gitriver-jwt
Смена значения в этом Secret доходит до сервера только с перезапуском подов: переменные окружения читаются один раз при старте.
Основные значения
| Значение | По умолчанию | Что задаёт |
|---|---|---|
image.repository, image.tag |
gitriver/gitriver, версия чарта |
образ выпуска; своё зеркало — здесь же |
database.host, .port, .user, .name |
— , 5432, gitriver, gitriver |
внешняя база |
database.password / .existingSecret |
— | пароль напрямую либо готовый Secret |
postgresql.enabled |
false |
поднять базу вместе с релизом (стенд) |
config.baseUrl |
— | внешний адрес установки |
config.ciLocalExecutor |
false |
встроенный исполнитель CI (нужен сокет Docker узла) |
config.ssh.enabled |
false |
встроенный SSH-сервер и порт ssh в Service |
service.ssh.port, .nodePort |
22, — |
порт SSH на Service; наружу решает service.type — LoadBalancer, NodePort (узловой порт, без него установка остановится — см. «Что чарт проверяет»), ClusterIP — только изнутри кластера |
config.ssh.containerPort, .publicPort, .publicHost |
2222, — , — |
порт SSH внутри пода; публичный порт и публичный хост в адресе для клонирования (порт пусто — из Service, хост — из config.baseUrl) |
config.existingJwtSecret.name, .key |
— , jwt-secret |
готовый Secret с ключом подписи токенов (обязателен под GitOps) |
config.jwtSecret |
— | ключ подписи значением; пусто — чарт сгенерирует при установке |
config.extraToml |
— | остальные параметры TOML: SMTP, LDAP, S3 |
config.extraEnv |
[] |
переменные GITRIVER_*, в том числе из Secret |
persistence.size, .storageClass, .accessModes |
50Gi, — , ReadWriteOnce |
том под данные |
ingress.* |
выключен | публикация наружу |
ingress.nginxDefaults |
true |
четыре nginx-аннотации чарта; свои annotations перекрывают те же ключи |
resources |
500m/1Gi запрос, 4Gi предел памяти |
доля узла под сервер (предела процессора нет намеренно) |
podSecurityContext, securityContext |
RuntimeDefault, снят весь набор возможностей кроме четырёх |
права пода |
tests.image |
— (образ сервера) | образ для helm test |
replicaCount |
1 |
число реплик (см. ниже) |
Всё остальное — helm show values gitriver-1.1.0.tgz.
С nginx-классом Ingress (в className есть “nginx”) чарт сам добавляет четыре
аннотации, без которых git через ingress-nginx не работает: контроллер
отбрасывает тело запроса больше 1 МБ — 413 Request Entity Too Large на
push — и закрывает проксируемый запрос через 60 секунд — клонирование
большого репозитория обрывается. Чарт снимает предел тела
(proxy-body-size: "0": пределы загрузки сервер держит сам), отключает
буферизацию запроса (proxy-request-buffering: "off": большой push уходит
к серверу потоком, не заполняя диск контроллера) и ставит таймауты чтения и
записи по часу (proxy-read-timeout, proxy-send-timeout). Свои аннотации
(ingress.annotations) перекрывают те же ключи. С другим контроллером или с
пустым className при nginx как классе по умолчанию — задайте эквиваленты в
annotations сами.
Что чарт проверяет до установки
Установка останавливается с объяснением, а не оставляет под в цикле
перезапусков, если: не задано подключение к базе; не задан пароль (ни у внешней
базы, ни у базы релиза); запрошено больше одной реплики на томе, который не
ReadWriteMany или которого нет вовсе; ключ подписи токенов задан дважды — и
значением, и готовым Secret; встроенный SSH включён, а Service — NodePort
без узлового порта service.ssh.nodePort (30000–32767; и без
config.ssh.publicPort) — порт, выбранный наугад, не назвать в адресе для
клонирования; ключ подписи токенов не задан, а рендер
идёт без кластера — прежнее значение оттуда не прочитать, и каждая синхронизация
GitOps выпускала бы новый ключ.
Ресурсы и права пода
Запросы ресурсов заданы по требованиям к установке: cpu: 500m, memory: 1Gi.
Предел задан только памяти (4Gi) — он защищает узел от разросшегося пода.
Предела процессора нет: с ним сервер замедлялся бы на всплесках нагрузки
(обход большого репозитория, обновление схемы при первом запуске), а долю
процессора на занятом узле обеспечивает запрос. Свои значения — resources в
values.yaml.
Под запускается от root и сам переходит к пользователю службы: он выравнивает
владельца тома и подключается к группе сокета Docker для встроенного
исполнителя CI. Поэтому runAsNonRoot для него невозможен, и права сужены
иначе — снят весь набор возможностей Linux, кроме CHOWN, DAC_OVERRIDE,
SETGID и SETUID, добавлен seccompProfile: RuntimeDefault и запрещено
повышение прав. Кластер с политикой restricted такой под отклонит: там
задайте своё исключение для этого пространства имён.
Проверка релиза (helm test) запускается на образе самого сервера — в закрытом
контуре он уже на месте. Свой образ (нужен только wget) — tests.image.
Несколько реплик
Сервер рассчитан на несколько реплик, но им нужно общее хранилище: том с
accessModes: [ReadWriteMany] и strategy.type: RollingUpdate. Подробнее —
«Несколько реплик». На ReadWriteOnce оставьте одну
реплику: стратегия по умолчанию (Recreate) как раз для этого случая — том не
отдаётся двум подам сразу.
CI в кластере
Встроенный исполнитель CI запускает задания в Docker на машине сервера, поэтому
в кластере он выключен: очередь забирают внешние исполнители. Нужен хотя бы один
исполнитель с меткой default — иначе задания ждут до ci_runner_max_wait_secs и
завершаются ошибкой. Установка исполнителей — в
руководстве по CI.
База данных вместе с релизом
Для стенда чарт поднимает базу сам — одиночный StatefulSet на официальном
образе postgres:
helm install gitriver gitriver-1.1.0.tgz \
--set postgresql.enabled=true \
--set postgresql.auth.password=<ПАРОЛЬ>
Внешних зависимостей у чарта нет: helm dependency update не нужен, и в контуре
без интернета достаточно образов gitriver и postgres во внутреннем реестре.
Для эксплуатации базу держат снаружи (database.host) или под оператором
PostgreSQL. База релиза не реплицируется, не резервируется и не переходит между
мажорными версиями PostgreSQL: смена postgresql.image на следующий мажорный
выпуск оставит том, который новый сервер не прочитает.
Обновление
helm upgrade gitriver gitriver-НОВАЯ-ВЕРСИЯ.tgz --reuse-values --set image.tag=НОВАЯ-ВЕРСИЯ
Схему базы сервер обновляет сам при старте. Ключ подписи токенов чарт переносит из прежнего Secret — выпущенные токены остаются действительными.
Удаление выпуска и данные
helm uninstall снимает объекты выпуска, но ТОМА ОСТАВЛЯЕТ — это защита от
случайной потери данных. После удаления в пространстве имён
остаются:
| Том | Что в нём |
|---|---|
<выпуск>-gitriver |
репозитории, реестр, артефакты сборок, страницы |
data-<выпуск>-gitriver-postgresql-0 |
база, если она развёрнута вместе с выпуском (postgresql.enabled=true) |
Кроме томов, остаётся Secret <выпуск>-gitriver-signing-key — ключ подписи
токенов; он помечен helm.sh/resource-policy: keep, как и тома, и повторная
установка под тем же именем подхватывает его.
Отсюда два следствия, и оба стоит знать заранее.
Место НЕ освобождается: пока тома живы, занятое ими остаётся занятым, и на общем кластере это заметят не сразу. Посмотреть, что осталось:
kubectl -n ПРОСТРАНСТВО get pvc
Удаление пространства имён УНОСИТ ДАННЫЕ. Вместе с томами уходит база, а с ней
— сведения об установке, к которым привязана активация лицензии: восстановить
её будет нечем, потребуется новая активация у издателя. Прежде чем чистить
«для порядка», сделайте резервную копию (gitriver backup) и убедитесь,
что она восстанавливается.
Осознанная полная очистка — после копии:
helm uninstall ВЫПУСК -n ПРОСТРАНСТВО
kubectl -n ПРОСТРАНСТВО delete pvc <выпуск>-gitriver data-<выпуск>-gitriver-postgresql-0
kubectl -n ПРОСТРАНСТВО delete secret <выпуск>-gitriver-signing-key
Для выпуска с именем gitriver имена без повтора приставки: тома gitriver и
data-gitriver-postgresql-0, Secret gitriver-signing-key.
Конфигурация
GitRiver настраивается через TOML-файл и/или переменные окружения GITRIVER_*.
Переменные окружения имеют приоритет над TOML. Где лежит файл, зависит от
способа установки: в образе — /var/lib/gitriver/gitriver.toml, из пакета —
/etc/gitriver/gitriver.toml, в Kubernetes его собирает чарт из values.yaml
(дописать своё — config.extraToml).
Генерация конфигурации
gitriver init-config --output gitriver.toml
Образец пишется на языке терминала (LANG, например LANG=en_US.UTF-8 — по-английски);
тот же образец на каждом языке лежит в пакете —
/usr/share/doc/gitriver/<язык>/gitriver.toml.example.
Образец называет все настройки установки: те, что не нужны, оставлены в нём закомментированными вместе с пояснением и значением по умолчанию. Строки, помеченные в образце словом «БЕЗОПАСНОСТЬ», меняют границы доступа — их читают целиком, прежде чем задавать значение. Образец полон: настройки, которой в нём нет, не существует и в продукте.
Основные параметры
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
host |
GITRIVER_HOST |
0.0.0.0 |
Адрес привязки |
port |
GITRIVER_PORT |
8080 |
HTTP-порт |
database_url |
GITRIVER_DATABASE_URL |
— | PostgreSQL URL |
jwt_secret |
GITRIVER_JWT_SECRET |
авто | Ключ подписи токенов (32+ байт) |
git_repos_path |
GITRIVER_GIT_REPOS_PATH |
/var/lib/gitriver/repos |
Каталог репозиториев |
web_dist_path |
GITRIVER_WEB_DIST_PATH |
— | Каталог с другой сборкой интерфейса. Не задан — отдаётся интерфейс, встроенный в бинарник |
base_url |
GITRIVER_BASE_URL |
http://{host}:{port} |
Внешний URL |
language |
GITRIVER_LANGUAGE |
ru |
Язык установки: на нём сервер отвечает, когда язык читателя неизвестен — запрос без заголовка Accept-Language, SSH, журнал задания CI, сохраняемые тексты. Запрос с Accept-Language получает свой язык, если он доступен (ru, en). Недоступный язык — отказ при старте |
trusted_proxies |
— | приватные сети | Адреса обратных посредников, чьим X-Real-IP/X-Forwarded-For можно доверять. Пустой список и список без единой разобранной записи означают то же, что незаданный: доверие приватным сетям (loopback, RFC1918, fc00::/7). «Не верить никому» этой настройкой не выражается |
rate_limit_exempt_cidrs |
GITRIVER_RATE_LIMIT_EXEMPT_CIDRS |
пусто | Адреса, освобождённые от пределов частоты (например, система мониторинга или собственный нагрузочный стенд). Снимает для названных адресов все пределы частоты, включая предел попыток входа, — список держат узким. Счёт неудачных попыток пароля (пять на пару «имя-адрес») это НЕ снимает: он считается отдельно. Подробнее: admin-guide |
pages_data_path |
GITRIVER_PAGES_DATA_PATH |
{repos}/../pages-data |
Хранение статических сайтов Pages |
Доступ и видимость
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
public_profiles |
GITRIVER_PUBLIC_PROFILES |
false |
Видны ли профили пользователей без входа. По умолчанию нет: анонимно читаемый список сотрудников с адресами почты — сведения о компании, которые она не собиралась публиковать. Действует одинаково на /api/v1 и на слои совместимости с чужими интерфейсами |
cors_origins |
— | только base_url |
Источники, которым разрешены запросы из браузера (CORS). Не задано или пустой список — разрешён ровно один источник, base_url. Задают, когда интерфейс живёт на другом адресе, чем API. Пишутся со схемой (https://git.example.com) — так, как браузер шлёт заголовок Origin; значение без схемы примется, но не совпадёт ни с одним запросом |
metrics_token |
GITRIVER_METRICS_TOKEN |
не задан | Токен доступа к /metrics. Задан — точка требует заголовка Authorization: Bearer <токен>; не задан — открыта. Метрики не называют репозиториев и людей, но сводные счётчики установки (сколько пользователей, репозиториев, находок безопасности) — сведения о жизни организации. На установке, доступной из интернета, токен задать следует |
Подключение к БД (альтернативное)
Вместо database_url можно указать отдельные параметры:
GITRIVER_DB_HOST=localhost
GITRIVER_DB_PORT=5432
GITRIVER_DB_USER=gitriver
GITRIVER_DB_PASS=secret
GITRIVER_DB_NAME=gitriver
Пределы загрузки файлов
Сколько сервер принимает за один запрос:
| Что загружается | Предел | Отказ при превышении |
|---|---|---|
| Файл пакета — PyPI, Cargo, Maven, NuGet, Generic | 500 МиБ | 413, текст называет заявленный размер и предел |
Публикация npm (npm publish) |
64 МиБ на весь запрос | 413 |
| Вложение релиза | 500 МиБ | 413; сверх квоты владельца — 403, см. квоту вложений релизов |
| ZIP статического сайта Pages | 500 МиБ сжатого, 1 ГиБ распакованного | 413 |
| Объект Git LFS | 5 ГиБ | 422; сверх квоты владельца — 403 |
| Слой образа в реестре образов | 10 ГиБ на слой целиком (не на запрос) | 413; сверх квоты — 403, см. квоту пакетного хранилища |
| Архив артефактов и кеша задания CI | 500 МиБ | 413; сверх квоты владельца — 403 |
git push по HTTP |
500 МиБ на запрос (сжатое тело) | 413; тело, распакованное сверх 500 МиБ, — 400 |
Предел npm ниже остальных: публикация npm целиком проходит через память сервера. 64 МиБ запроса — это примерно 48 МиБ архива пакета. Остальные загрузки идут потоком на диск, и память сервера от размера файла не зависит.
Предел слоя образа выше остальных: слои базовых образов (окружения машинного обучения, CUDA) исчисляются гигабайтами. Считается он по слою целиком, сколько бы запросов ни потребовалось на его передачу. Сколько места слой получит на самом деле, решает квота; предел действует и там, где квота не задана.
Значения заданы в самом продукте и настройками не меняются. Настраивается срок загрузки:
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
upload_http_timeout_secs |
GITRIVER_UPLOAD_HTTP_TIMEOUT_SECS |
3600 |
Срок запроса, загружающего файл (публикация пакета, вложение релиза, ZIP сайта, архив артефактов и кеша задания CI). Отдельный от общего срока API в 30 секунд: большой файл в него не укладывается |
ai_http_timeout_secs |
GITRIVER_AI_HTTP_TIMEOUT_SECS |
660 |
Срок запроса, ожидающего ответа модели (разбор упавшего задания, разбор запроса на слияние, проверка связи с поставщиком). Отдельный от общего срока API в 30 секунд: рассуждающая модель думает дольше, а помощнику разрешено ждать до 600 секунд |
Этим же значением ограничен срок, на который загрузка занимает место в квоте: дольше запроса резерв не живёт, поэтому оборванное соединение и перезапуск сервера не держат место навсегда (см. место занимается на время приёма).
Обратный прокси обязан пропускать столько же. У nginx
client_max_body_sizeпо умолчанию 1 МиБ, и загрузка отвалится на нём, не дойдя до сервера (в примере конфигурации выше —512m).
Помимо предела на запрос действует квота владельца — пакеты, Pages, LFS, CI-артефакты и CI-кеш считаются в ней вместе по всем репозиториям; см. руководство администратора.
CI/CD
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
ci_data_path |
GITRIVER_CI_DATA_PATH |
{repos}/../ci-data |
Журналы и рабочие каталоги заданий |
ci_max_concurrent_jobs |
GITRIVER_CI_MAX_CONCURRENT_JOBS |
доступный ЦП / ci_docker_cpus, не больше 4 |
Предел параллельных заданий. Умолчание считается от доступного ЦП — числа ядер, ограниченного квотой cgroup: при ci_docker_cpus = 2 четырёхъядерный сервер берёт два задания, восьмиъядерный — четыре (см. Пределы ресурсов задания) |
ci_local_executor_enabled |
GITRIVER_CI_LOCAL_EXECUTOR |
true |
Встроенный исполнитель (задания в Docker на сервере). false — сервер задания не исполняет и Docker ему не нужен: вся очередь, включая runs-on: default и задания без runs-on, уходит внешним исполнителям |
ci_job_timeout_secs |
GITRIVER_CI_JOB_TIMEOUT_SECS |
3600 |
Срок задания (секунды) |
ci_runner_offline_after_secs |
GITRIVER_CI_RUNNER_OFFLINE_AFTER_SECS |
180 |
Через сколько секунд молчания внешний исполнитель считается вышедшим из сети. Исполнитель отмечается при каждом опросе очереди (--poll-interval, по умолчанию 5 с); поднимайте значение, если опрос у исполнителей реже |
ci_pipeline_retention_days |
GITRIVER_CI_PIPELINE_RETENTION_DAYS |
90 |
Хранение конвейеров (0 — навсегда) |
ci_cache_retention_days |
GITRIVER_CI_CACHE_RETENTION_DAYS |
14 |
Хранение архива cache: без обращений (0 — не вытеснять по сроку) |
ci_docker_memory |
GITRIVER_CI_DOCKER_MEMORY |
2g |
Предел памяти задания: docker --memory у задания с image:, memory.max cgroup у задания без образа |
ci_docker_cpus |
GITRIVER_CI_DOCKER_CPUS |
2 |
Предел CPU контейнера задания. Сборка через docker build его не соблюдает — см. Пределы ресурсов задания |
ci_job_pids_limit |
GITRIVER_CI_JOB_PIDS_LIMIT |
512 |
Предел числа процессов и потоков задания: docker --pids-limit у задания с image:, pids.max cgroup у задания без образа. 0 — не ограничивать |
ci_cgroup_parent |
GITRIVER_CI_CGROUP_PARENT |
gitriver.slice |
Срез ресурсов CI: cgroup, в которую идут ВСЕ контейнеры заданий. Пустая строка — без среза. Предел самому срезу задаёт systemd, а не этот файл — см. Пределы ресурсов задания |
ci_eraser_image |
GITRIVER_CI_ERASER_IMAGE |
busybox |
Образ, которым снимается рабочий каталог задания, если его контейнер оставил в нём файлы от root (см. Уборка каталогов заданий) |
allowed_external_action_domains |
GITRIVER_ALLOWED_EXTERNAL_ACTION_DOMAINS |
не задано | Откуда разрешено брать внешние действия (uses: host/owner/repo@ref). Не задано — разрешён любой домен; [] — запрет всех. Записи: git.example.com (весь домен), git.example.com/actions (один владелец), git.example.com/actions/checkout (репозиторий) |
ci_docker_runtime |
GITRIVER_CI_DOCKER_RUNTIME |
default |
Среда запуска контейнеров задания: default — обычный Docker, sysbox — DinD без прав root через sysbox-runc, rootless — Docker без прав root, privileged — полный DinD. Последнее означает выход задания на хост и включается только там, где заданиям доверяют как администратору сервера |
ci_host_data_path |
GITRIVER_CI_HOST_DATA_PATH |
автоопределение | Хостовый путь ci_data_path для Docker-in-Docker. Обычно не нужен: путь определяется через docker inspect |
ci_git_clone_timeout_secs |
GITRIVER_CI_GIT_CLONE_TIMEOUT_SECS |
120 |
Срок git clone и checkout в задании |
ci_lfs_fetch_timeout_secs |
GITRIVER_CI_LFS_FETCH_TIMEOUT_SECS |
600 |
Срок git lfs pull в задании. Отдельный от клонирования: объекты LFS весят сотни мегабайт |
ci_after_script_timeout_secs |
GITRIVER_CI_AFTER_SCRIPT_TIMEOUT_SECS |
300 |
Срок after_script задания |
ci_runner_poll_secs |
GITRIVER_CI_RUNNER_POLL_SECS |
3 |
Как часто сервер проверяет, какие задания конвейера готовы к запуску, секунды |
ci_runner_max_wait_secs |
GITRIVER_CI_RUNNER_MAX_WAIT_SECS |
3600 |
Сколько задание ждёт свободного исполнителя |
ci_job_token_ttl_secs |
GITRIVER_CI_JOB_TOKEN_TTL_SECS |
28800 |
Срок жизни CI_JOB_TOKEN. Должен быть не меньше самого долгого срока задания в конвейере |
Контейнерный реестр и сканирование образов
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
ci_registry_host |
GITRIVER_CI_REGISTRY |
из base_url |
Адрес реестра для переменной CI_REGISTRY. Указывайте 127.0.0.1, если Docker BuildKit не может подключиться к localhost (IPv6 против IPv4) |
registry_scan_enabled |
— | false |
Автоматическое сканирование образов на уязвимости (Trivy) при публикации манифеста. Сканированием по требованию не управляет: его просит человек с правом записи в реестр |
trivy_path |
— | trivy |
Путь к исполняемому файлу Trivy |
registry_token_expiry_secs |
GITRIVER_REGISTRY_TOKEN_EXPIRY_SECS |
7200 |
Срок жизни токена реестра. Заметно длиннее прочих: BuildKit держит токен на всю сборку |
Хранилище реестра задаётся секцией [s3]; без неё
образы лежат на файловой системе.
Лицензирование
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
license_server_url |
GITRIVER_LICENSE_SERVER_URL |
https://gitriver.ru |
Адрес сервера лицензий: туда уходит отметка установки, оттуда приходит подписанный ответ о продлении и отзыве |
update_check_url |
GITRIVER_UPDATE_CHECK_URL |
https://gitriver.com/releases/latest |
Откуда установка узнаёт о вышедших версиях. Закрытому контуру сюда ставят своё зеркало, установке за посредником — адрес посредника |
update_check_enabled |
GITRIVER_UPDATE_CHECK_ENABLED |
true |
Спрашивать ли о новых версиях. Для бесплатной установки это единственное обращение наружу; false выключает его целиком |
Адрес без https:// принимается — закрытый контур ставит свой сервер, — но
сервер называет это в журнале при каждом старте: в отметке идут сведения об
установке. Что именно передаётся — licensing.
Сроки запросов и сроки жизни токенов
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
http_request_timeout_secs |
GITRIVER_HTTP_REQUEST_TIMEOUT_SECS |
30 |
Обычный запрос /api/v1 |
git_http_timeout_secs |
GITRIVER_GIT_HTTP_TIMEOUT_SECS |
3600 |
git clone и git push по HTTP: короткий срок разорвал бы клонирование большого репозитория |
lfs_http_timeout_secs |
GITRIVER_LFS_HTTP_TIMEOUT_SECS |
3600 |
Выгрузка и загрузка объектов LFS |
registry_http_timeout_secs |
GITRIVER_REGISTRY_HTTP_TIMEOUT_SECS |
3600 |
Операции реестра образов |
oauth_provider_timeout_secs |
GITRIVER_OAUTH_PROVIDER_TIMEOUT_SECS |
30 |
Обращения к поставщику OAuth2 |
webhook_timeout_secs |
GITRIVER_WEBHOOK_TIMEOUT_SECS |
10 |
Запрос к получателю webhook |
webhook_connect_timeout_secs |
GITRIVER_WEBHOOK_CONNECT_TIMEOUT_SECS |
5 |
Установление соединения с получателем webhook |
sse_keepalive_secs |
GITRIVER_SSE_KEEPALIVE_SECS |
15 |
Интервал контрольных сообщений потока событий (SSE) |
lfs_token_ttl_secs |
GITRIVER_LFS_TOKEN_TTL_SECS |
900 |
Срок жизни токена LFS |
oauth_token_expiry_secs |
GITRIVER_OAUTH_TOKEN_EXPIRY_SECS |
3600 |
Срок жизни токенов OAuth и JWT |
Сроки загрузки файлов и запросов к помощнику — в разделе Пределы загрузки файлов; сроки заданий CI — в разделе CI/CD.
Пределы ресурсов задания
Задание встроенного исполнителя выполняется под пределами памяти и числа процессов. Значения и поведение при их исчерпании описаны в руководстве по CI.
Сколько заданий идёт разом. ci_max_concurrent_jobs по умолчанию считается
от того, сколько ЦП установке ДОСТУПНО: сколько заданий умещается при пределе
ci_docker_cpus на каждое (доступное делится на предел), но не больше четырёх.
Четырёхъядерный сервер с умолчаниями берёт два задания, восьмиъядерный — четыре.
Доступное — это не только число процессоров: квота cgroup тоже потолок.
Шестнадцатиядерная машина со срезом CPUQuota=600% (см. ниже) даёт заданиям
шесть ядер, и умолчание считается от шести. Квота читается при старте, поэтому
после systemctl set-property службу нужно перезапустить.
Заданное руками число сервер не поправит, но скажет при старте, если обещанного ЦП больше, чем доступно:
ci_max_concurrent_jobs = 4 при ci_docker_cpus = 2 обещает заданиям 8 ядер,
а доступно 4
Переобещание стоит дороже, чем кажется: машина, перегруженная сборками встроенного исполнителя, перестаёт отвечать и по API, и по git — снаружи это выглядит как зависание всей установки, а не как медленный CI.
Предел по заданию считает не весь расход. ci_docker_memory и
ci_docker_cpus получает КОНТЕЙНЕР задания. Шаг docker build внутри задания
исполняет не он, а демон BuildKit в отдельном контейнере, и пределы задания на
него не распространяются. Пока задания собирают образы, предел на задание
ограничивает только меньшую часть расхода.
Срез ресурсов CI
Поверх пределов задания стоит срез — общая cgroup, в которую попадает всё, что запускает сам исполнитель:
| Что | Как попадает в срез |
|---|---|
Сам сервер и задания без image: (идут в его процессе) |
Slice=gitriver.slice в unit-файле службы |
Контейнер задания, after_script, сервисы services: |
--cgroup-parent от настройки ci_cgroup_parent |
Контейнеры действий (uses:), которые поднимает скрипт задания |
то же, через переменную CI_CGROUP_PARENT |
| Служебные контейнеры уборки рабочей копии | то же |
| Внешний исполнитель на этой же машине | Slice=gitriver.slice и --cgroup-parent |
Имя среза по умолчанию — gitriver.slice, и оно должно быть одним у всех
перечисленных: иначе сервер окажется в одном срезе, а контейнеры его заданий —
в другом. Первая строка таблицы выполняется только там, где службу поднимает
unit-файл поставки: у сервера, запущенного из терминала или иным способом, в
срез попадут контейнеры заданий, но не он сам и не задания без image:.
Имя записывается в виде *.slice — такое значение принимают оба драйвера
cgroup демона docker. Абсолютный путь cgroup тоже принимается, но работает
только при драйвере cgroupfs: драйвер systemd (умолчание современных
дистрибутивов) его отвергает. Непригодное значение сервер называет в журнале
при старте и работает БЕЗ среза, а задания выполняются как обычно.
При старте сервер сверяет срез с драйвером cgroup демона docker и называет в журнале, что вышло:
срез ресурсов CI: gitriver.slice; отведено ему: ЦП — 6 ядер, память — 12288 МиБ
Если демон cgroup не управляет вовсе (Cgroup Driver: none — Docker без прав
root и без делегирования, cgroup v1), срез снимается с предупреждением в
журнале. Строка «его cgroup серверу
недоступна» означает другое — контейнеры в срез попадут, а вот пределы среза,
метрики и строки о его срабатываниях в
журналах заданий читаться не будут. У сервера в контейнере это лечится
монтированием /sys/fs/cgroup на чтение (см. «Установка в Docker» ниже).
Предел срезу задаёт systemd, а не gitriver.toml. Такой настройки в
gitriver.toml нет; предел задаётся командой:
systemctl set-property gitriver.slice CPUQuota=600% MemoryMax=12G
Оставьте машине запас сверх среза — ОС, базе и обратному посреднику; пример выше
рассчитан на машину о восьми ядрах и 16 ГиБ. Правка через set-property
переживает обновление пакета.
Без предела срез остаётся местом учёта — и это уже полезно:
systemd-cgtop gitriver.slice
То же самое, не заходя на машину, показывает админка: «Система» → «Срез ресурсов CI» — имя, пределы, занятая память и счётчики срабатываний, а во времени — метрики.
Сервер получает свою долю внутри среза. Чтобы API и git отвечали и на
машине, перегруженной сборками, unit-файл поставки даёт службе вес ЦП выше,
чем у контейнеров-соседей по срезу (CPUWeight=10000 против умолчания 100),
и защиту страниц от вытеснения (MemoryLow=512M). Это не резервирование: пока ресурсов хватает, задания берут
всё свободное.
Установка в Docker — две строки, и обе обязательны. Служба gitriver в
docker-compose.prod.yml объявляет cgroup_parent: gitriver.slice — тем же
именем, что уходит серверу в GITRIVER_CI_CGROUP_PARENT, — и монтирует
иерархию cgroup на чтение:
cgroup_parent: ${GITRIVER_CI_CGROUP_PARENT-gitriver.slice}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
Первая кладёт сервер в срез. Вторая даёт ему срез УВИДЕТЬ. Забыть вторую — не отказ, а тихая потеря половины: контейнеры заданий в срез попадут, а пределы среза, метрики по нему и строки о его срабатываниях в журналах заданий пропадут. Сервер говорит об этом при старте прямо:
срез ресурсов CI: gitriver.slice; его cgroup серверу недоступна — пределы среза
и метрики по нему не читаются, строк о его срабатываниях в журнале заданий не будет
Монтирование только на чтение. Новых прав оно не даёт: рядом уже смонтирован
docker.sock, а он равносилен root на машине.
Предел задаётся срезу НА ХОСТЕ той же командой systemctl set-property: внутри
контейнера systemd нет.
Установка обновляется — compose обновите тоже. Файл на вашей машине —
копия, сделанная при установке: docker compose pull && up -d берёт её, а не ту,
что приехала с новым выпуском. Обе строки выше нужно перенести в свой файл руками.
Сборка образов в срез не попадает, и это не настраивается на сервере.
Сборщик образов, который заводит сам конвейер (docker buildx create),
работает вне среза и вне пределов задания; параметр
--driver-opt cgroup-parent=… его в срез не переносит. Шаг docker build без
buildx исполняет сам демон docker, и пределы ни среза, ни службы
docker.service на него не действуют.
Что остаётся администратору:
- потребовать от авторов конвейеров пределов самому сборщику —
--driver-opt memory=6g --driver-opt cpu-quota=300000(см. руководство по CI); - либо задать умолчание демону: ключ
cgroup-parentв/etc/docker/daemon.json. Оно подействует на ВСЕ контейнеры машины, поэтому годится там, где машина отдана GitRiver целиком; - либо вынести сборку образов на внешний исполнитель отдельной машины — тогда её расход ограничен этой машиной, а не соседством с сервером.
Задание с image: получает пределы от Docker — ничего настраивать не нужно.
Задание без image: выполняется скриптом на самом сервере, и пределы ему
накладывает cgroup v2. Для этого службе нужно делегированное поддерево cgroup —
строка в unit-файле:
[Service]
Delegate=cpu pids memory
В пакете эта строка уже стоит — при установке из deb/rpm настраивать нечего.
Сервер в Docker-контейнере получает поддерево, если /sys/fs/cgroup
смонтирован на запись (--cgroupns=private и rw-монтирование cgroup2).
Проверить, вышло ли, можно по журналу сервера: если поддерево недоступно, при первом же задании появляется предупреждение
cgroup v2 серверу недоступна: пределы числа процессов и памяти задач БЕЗ `image:`
не накладываются. Задачам с `image:` пределы задаёт Docker. Как включить —
руководство по установке (/usr/share/doc/gitriver/<язык>/installation.md),
раздел «Пределы ресурсов задания»
Задание без image: в этом случае выполняется без пределов памяти и
процессов. Число одновременных заданий при этом не понижается: умолчание
ci_max_concurrent_jobs считается от ci_docker_cpus, а этот предел
накладывает Docker. Установке, где пределы обязательны для всех заданий,
следует требовать image: у заданий либо выключить встроенный исполнитель и
отдать очередь внешним исполнителям.
Уборка каталогов заданий
Скрипт задания с image: выполняется в контейнере от root, и файлы, созданные
им в рабочем каталоге, принадлежат root или вовсе чужому uid (так бывает у
docker buildx). У образов с umask 0077 (например redis:7) закрытыми
оказываются и каталоги — пользователь сервера удалить их не может.
Поэтому сервер убирает такие каталоги контейнером: сначала образом самого
задания, а если этого не хватило (демон с userns-remap, файлы buildx) или
образ задания уже неизвестен (уборка завершённых заданий раз в час) —
служебным образом-«ластиком», который удаляет файлы от root. Так же убирает
каталоги и внешний исполнитель
(руководство администратора).
Образ по умолчанию — busybox (около 4 МБ). Если его на машине нет, сервер
загружает его при первой надобности; в закрытом контуре укажите свой образ из
доступного реестра — подойдёт любой с sh, find и rm:
ci_eraser_image = "registry.example.com/base/busybox:1.36"
Недоступность образа не молчит: каталог остаётся на диске, а в журнал уходит строка с причиной и путём.
Лёгкая установка без Docker (только внешние исполнители)
По умолчанию задания CI выполняет сам сервер — встроенным исполнителем, в
контейнерах Docker. Настройка ci_local_executor_enabled = false выключает его
и отдаёт всю очередь внешним исполнителям:
GITRIVER_CI_LOCAL_EXECUTOR=false
ci_local_executor_enabled = false
Что это даёт. Серверу больше не нужен ни Docker, ни доступ к его сокету:
GitRiver ставится на лёгкую машину и в окружение, где Docker запрещён
политикой. Освобождаются процессор и память, которые иначе уходили бы на
сборки — сервер занят только git, базой и HTTP, а пики нагрузки от сборок
переносятся на машины исполнителей. Сервер не обращается к Docker вовсе:
уборка контейнеров сборщиков и обслуживание кеша сборки не запускаются, пределы
ci_docker_memory, ci_docker_cpus и ci_job_pids_limit не действуют, а локальный предел
параллельности ci_max_concurrent_jobs не занимается.
Что всё равно требуется. Диск: журналы и артефакты заданий исполнители загружают
на сервер, в ci_data_path — их объём от режима не зависит. Рассчитывайте
место так же, как для встроенного исполнителя, и сохраняйте
ci_pipeline_retention_days (срок хранения конвейеров) осмысленным. Кеш между
запусками (cache:) работает и на внешних исполнителях — архивом обменивается сам
сервер, — поэтому он тоже занимает ci_data_path: его объём ограничивает квота
владельца (max_ci_cache), а забытые ключи снимает срок
ci_cache_retention_days.
Обязательное условие — исполнитель с меткой default. В этом режиме метки заданий
не рассматриваются: исполнитель нужен всей очереди, включая runs-on: default и
задания без runs-on. Задание без runs-on заберёт любой исполнитель, а
runs-on: default — только тот, у которого метка default объявлена.
Зарегистрируйте хотя бы одного такого исполнителя (админка → «Исполнители»), иначе задания
будут ждать до ci_runner_max_wait_secs (по умолчанию час) и завершаться
ошибкой: запасного варианта «выполнить на сервере» в этом режиме нет. Пока
подходящего исполнителя нет, админка и мастер настройки показывают предупреждение.
Как поставить исполнителя на сборочную машину —
«Установка внешнего исполнителя».
Установка самих исполнителей и поведение заданий описаны в руководстве по CI.
SMTP (уведомления по почте)
[smtp]
host = "smtp.example.com"
port = 587
username = "gitriver@example.com"
password = "пароль"
from = "gitriver@example.com"
starttls = true
Шифрование задаётся одним полем и имеет три состояния:
starttls |
Порт | Соединение |
|---|---|---|
true (умолчание) |
любой | StartTLS — соединение поднимается до TLS после приветствия |
false |
465 | неявный TLS (SMTPS): шифрование до приветствия |
false |
любой другой | без шифрования — для внутреннего релея в закрытом контуре |
Последний случай сервер называет в журнале при каждом старте: пароль учётной записи релея и тела писем идут по сети открытым текстом.
Почта настраивается и в панели администратора; её значения имеют приоритет, а секция читается, когда в базе настроек ещё нет.
LDAP (корпоративная авторизация)
[ldap]
url = "ldaps://ldap.example.com:636"
# Для url = "ldap://..." соединение поднимается до TLS через StartTLS.
# Значение по умолчанию — true; false оставляет пароли открытым текстом.
# starttls = true
bind_dn = "cn=service,dc=example,dc=com"
bind_password = "пароль"
search_base = "ou=users,dc=example,dc=com"
user_filter = "(uid={login})"
email_attr = "mail"
display_name_attr = "displayName"
admin_group_dn = "cn=admins,ou=groups,dc=example,dc=com"
# link_existing_users = false
display_name_attr — атрибут отображаемого имени (displayName, cn). Не
задан — отображаемого имени у записи не будет, и везде показывается имя входа.
link_existing_users решает, вправе ли вход через каталог занять существующую
локальную учётную запись с тем же именем. По умолчанию нет: имя в каталоге
задаёт сторона каталога, и совпадение с локальным admin открывало бы его
учётную запись мимо её пароля. Записи, заведённые через каталог, это не
затрагивает. Включают при переезде, когда локальные имена намеренно совпадают
с именами в каталоге.
Настройки каталога из панели администратора хранятся в базе и имеют приоритет над этой секцией: она читается, когда в базе настроек ещё нет.
S3 (реестр образов)
[s3]
endpoint = "https://s3.example.com"
bucket = "gitriver-registry"
access_key = "ACCESS_KEY"
secret_key = "SECRET_KEY"
region = "us-east-1"
# part_size_mb = 5
part_size_mb — размер части многочастной загрузки, МиБ (по умолчанию 5,
допустимо от 5 до 100). Больше — меньше запросов к хранилищу на тот же объём;
меньше — быстрее повтор части при обрыве.
Хранилище реестра настраивается и в панели администратора. Как только оно задано там, секция
[s3]не читается вовсе — сервер говорит об этом строкой в журнале при старте. Правка секции в такой установке не даёт ничего.
SSH-доступ
GitRiver поддерживает SSH для клонирования и отправки. Настройка:
- В профиле пользователя: Настройки / SSH-ключи — добавить публичный ключ
- Дальше — по способу установки:
Встроенный SSH-сервер (системный sshd не нужен):
ssh_port = 2222
[ssh_server]
listen = "0.0.0.0:2222"
host_key_path = "/var/lib/gitriver/ssh_host_ed25519_key"
В Docker порт публикуется наружу (GITRIVER_SSH_PORT в compose), в Kubernetes —
значением config.ssh.enabled=true.
Системный sshd: сервер сам поддерживает authorized_keys учётной записи
службы. Путь зависит от способа установки — из пакета домашний каталог
gitriver это /var/lib/gitriver:
authorized_keys_path = "/var/lib/gitriver/.ssh/authorized_keys"
Клонирование: git clone ssh://git@git.example.com/owner/repo.git
Сеанс через системный sshd ведёт команда gitriver serv. Её журнал пишется в
системный журнал (syslog, под именем gitriver-serv) — рядом со строками входа
самого sshd: journalctl -t gitriver-serv. Клиенту в ответ уходит только
причина отказа.
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
authorized_keys_path |
— | не задан | Файл authorized_keys, в который сервер записывает ключи пользователей. Не задан — управление authorized_keys отключено |
ssh_host |
GITRIVER_SSH_HOST |
из base_url |
Публичный хост в адресе для клонирования |
ssh_port |
GITRIVER_SSH_PORT |
порт [ssh_server], иначе 22 |
Публичный порт в адресе для клонирования. Не задан — берётся порт встроенного сервера из listen, а без него 22. Для нестандартного порта адрес принимает вид ssh://git@host:port/owner/repo.git |
ssh_user |
GITRIVER_SSH_USER |
git |
Имя пользователя в адресе для клонирования |
ssh_enabled |
GITRIVER_SSH_ENABLED |
автоматически | Показывать ли вкладку SSH в окне «Клонировать». По умолчанию видна, если задан authorized_keys_path или секция [ssh_server]; явное значение решает за оба случая — например, когда SSH обслуживает отдельный внешний демон. Настройка вкладки в панели администратора старше этого значения |
commit_signing_key_path |
GITRIVER_COMMIT_SIGNING_KEY_PATH |
рядом с файлом настроек | Ключ Ed25519, которым сервер подписывает коммиты слияния, сделанные им самим (слияние в интерфейсе или через API). Создаётся при первом запуске. Нужен, чтобы при require_signed_commits вершина защищённой ветки оставалась подписанной после слияния; без ключа коммиты слияния идут без подписи |
listen (в [ssh_server]) |
GITRIVER_SSH_SERVER_LISTEN |
обязателен | Адрес и порт встроенного SSH-сервера. Переносится в базу при первом старте с секцией; дальше значением владеет панель администратора |
host_key_path (в [ssh_server]) |
GITRIVER_SSH_SERVER_HOST_KEY_PATH |
рядом с файлом настроек | Ключ Ed25519 самого SSH-сервера. Создаётся при первом запуске. Переменная действует только при включённом встроенном сервере: одна она, без секции [ssh_server] и без GITRIVER_SSH_SERVER_LISTEN, не включает ничего |
Панель администратора старше файла. Публичные
ssh_host,ssh_port,ssh_userи видимость вкладки задаются ещё и в разделе SSH панели администратора; заданное там значение вытесняет значение из файла и применяется без перезапуска. Значения из файла работают, пока в панели ничего не задано.С секцией
[ssh_server]строже: она не управление, а разовый посев. При первом старте с этой секцией её значения переносятся в базу (в журнале —ssh bootstrap: TOML [ssh_server] перенесён в БД), и дальше встроенным сервером владеет панель: правка секции в файле уже ничего не меняет. То же относится кGITRIVER_SSH_SERVER_LISTENиGITRIVER_SSH_SERVER_HOST_KEY_PATH— они задают ту же секцию.
Несколько реплик
Сервер рассчитан на запуск в нескольких экземплярах за балансировщиком (в Kubernetes — несколько подов одного Deployment). Требования к окружению:
- Общая БД — все реплики подключены к одному PostgreSQL.
- Общее хранилище для репозиториев, данных CI, реестра образов и Pages:
сетевой том с одновременным доступом на запись (в Kubernetes —
ReadWriteMany) либо S3 для реестра. Локальный диск пода не подойдёт: запрос на скачивание артефакта попадёт на любую реплику.
Фоновые задачи выполняются в одной реплике. Планировщик CI, очередь на слияние, очереди групп одновременности CI, очистки артефактов, конвейеров и реестра, опрос GitOps, контроллер исполнителей в Kubernetes, запросы подтверждения развёртываний в окружения, резервные копии по расписанию, отметка лицензии и последствия истечения лицензий (снятие мест Pro, запись в ленту) каждая выполняется только в одной реплике, и долгая работа одной задачи не задерживает остальные. Реплика, потерявшая связь с базой или остановленная, освобождает свои задачи, и со следующего прохода их подхватывает другая — вмешательства администратора не требуется, отдельного «главного» пода в конфигурации нет.
Отправка почты из очереди и повторные доставки вебхуков работают во всех
репликах и делят нагрузку. Чистка мусора Docker
(docker volume/image/network prune) идёт на
каждой машине со встроенным исполнителем CI — демон у каждой свой.
Выкатка новой версии. Остановленный под сразу отдаёт свои фоновые задачи, поэтому скользящее обновление не оставляет их без присмотра дольше, чем на один их интервал.
Резервное копирование
Путь один — подкоманды сервера. Они делают полную копию (база, репозитории, реестр, CI, Pages), проверяют целостность архива при восстановлении и возвращают ненулевой код, если восстановить не удалось. Отдельных сценариев копирования в поставке нет; копируйте и восстанавливайте только этими подкомандами — своя обёртка может пропустить проверку целостности архива.
Настройки
| Параметр | Env | По умолчанию | Описание |
|---|---|---|---|
backup_encryption_key |
GITRIVER_BACKUP_ENCRYPTION_KEY |
не задан | Ключ шифрования копий (base64, 32 байта). Задан — архив шифруется AES-256-GCM |
backup_token_ttl_secs |
GITRIVER_BACKUP_TOKEN_TTL_SECS |
60 |
Срок жизни одноразовой ссылки на скачивание копии |
Ключ шифрования хранят отдельно от копий: вместе они не защищают ни от чего, а порознь потеря ключа означает потерю копий. Смена ключа не перешифровывает уже сделанные архивы — восстановить их прежним ключом можно, новым нельзя.
Создание резервной копии
gitriver backup --config gitriver.toml --output backup.tar.gz
Включает: базу, репозитории, реестр, данные CI, Pages.
Флаги:
--repos false— исключить репозитории--registry— включить реестр образов--ci— включить данные CI--pages— включить Pages
Восстановление
gitriver restore --config gitriver.toml backup.tar.gz
Автоматическое расписание
Настраивается в интерфейсе: Администрирование / Резервное копирование / Расписание.
Обновление
# образ
cd /opt/gitriver
docker compose pull gitriver
docker compose up -d gitriver
# пакет
sudo apt install ./gitriver_НОВАЯ-ВЕРСИЯ_amd64.deb # или dnf upgrade
# Helm
helm upgrade gitriver gitriver-НОВАЯ-ВЕРСИЯ.tgz --reuse-values --set image.tag=НОВАЯ-ВЕРСИЯ
Схему базы сервер обновляет сам при старте — отдельного шага обновления нет ни в одном из трёх способов. Если сервер остановился с сообщением об обновлении схемы — см. «Решение проблем» ниже.
Переменная GITRIVER_RATE_LIMIT_DISABLED не действует. Пределы частоты
снимаются настройкой rate_limit_exempt_cidrs
(GITRIVER_RATE_LIMIT_EXEMPT_CIDRS), которая перечисляет освобождённые адреса.
Если переменная у вас задана, после обновления пределы частоты действуют для
всех — сервер скажет об этом в журнале при старте; перенесите нужные адреса в
rate_limit_exempt_cidrs.
Решение проблем
Мастер настройки не появляется
Убедитесь, что database_url не указан в TOML и env. Мастер запускается только при отсутствии подключения к БД.
Ошибки токенов после перезапуска
Ключ подписи токенов хранится в /var/lib/gitriver/.jwt_secret (из пакета —
/etc/gitriver/.jwt_secret). При необходимости укажите jwt_secret явно в
конфиге. В Kubernetes файла нет: ключ приходит переменной
GITRIVER_JWT_SECRET из Secret — см.
«Ключ подписи токенов». Первый подозреваемый там —
синхронизация GitOps с ключом, который чарт генерирует сам.
Ключ теряется — теряются не только сеансы. Вместе с ним становится нечитаемым всё, что зашифровано в базе на его основе:
| Что зашифровано | Что будет |
|---|---|
| TOTP-секреты пользователей | вторая ступень входа перестанет работать у всех, кто её включил; сбрасывать придётся администратору по одному |
client_secret приложений OAuth (GitRiver как поставщик) |
подключённые приложения перестанут получать токены, секреты нужно выпустить заново |
| Токены доступа к внешним реестрам в кеше посредника | выдача приватных источников через посредника остановится до повторного ввода токенов |
| Ключ доступа к поставщику языковой модели | помощник перестанет отвечать до повторного ввода ключа |
Ключ подписи id_token (GitRiver как поставщик OIDC) |
установка заведёт новый и опубликует его в JWKS; выданные ранее id_token перестанут проверяться, получатели переживут это как разовый повторный вход |
Поэтому .jwt_secret — часть резервной копии, а не одноразовый файл, и менять
его «на всякий случай» не следует. Замена оправдана при утечке ключа; тогда
перечисленное выше нужно вводить заново осознанно, а не обнаруживать по жалобам
пользователей.
Задания CI не запускаются
- Сокет Docker доступен:
-v /var/run/docker.sock:/var/run/docker.sock - Пользователь
gitriverв группе docker (в образе это делается при запуске само) - Проверьте
ci_max_concurrent_jobs— возможно предел достигнут - Если
ci_local_executor_enabled = false— задания ждут внешнего исполнителя. Откройте админку → «Исполнители»: там показано, есть ли исполнитель в сети и объявлена ли у него меткаdefault(без неё задания сruns-on: defaultбрать некому). См. «Лёгкая установка без Docker»
Отправка в git отклоняется
- Проверьте защиту веток (настройки репозитория → «Защита веток»)
- Для LFS: установите
client_max_body_sizeв nginx
Сервер не стартует: «Найдены пакеты NuGet, различающиеся только регистром»
Идентификаторы NuGet регистронезависимы: Newtonsoft.Json и newtonsoft.json —
один и тот же пакет. Если в одном репозитории нашлись две записи такого пакета,
сервер не сливает их сам — у одинаковых версий могут различаться файлы и
метаданные — и останавливает обновление, пока вы не сведёте их вручную.
Найдите конфликтующие записи:
SELECT r.name AS repo, LOWER(p.name) AS package_id,
ARRAY_AGG(p.name ORDER BY p.created_at) AS variants
FROM packages p
JOIN repositories r ON r.id = p.repo_id
WHERE p.type = 'nuget'
GROUP BY r.name, p.repo_id, LOWER(p.name)
HAVING COUNT(*) > 1;
Для каждой группы выберите написание, которое остаётся (обычно — опубликованное первым), и перенесите версии из остальных записей:
BEGIN;
UPDATE package_versions SET package_id = '<id остающегося пакета>'
WHERE package_id = '<id лишнего пакета>';
DELETE FROM packages WHERE id = '<id лишнего пакета>';
COMMIT;
Если одна и та же версия опубликована в обеих записях, UPDATE завершится
ошибкой package_versions_package_id_version_key: это две разные публикации
одного номера версии. Решите, какая из них верна, удалите вторую
(DELETE FROM package_versions WHERE id = ...) и повторите перенос.
После слияния перезапустите сервер — обновление продолжится.
Сервер не стартует: «содержит несколько эквивалентных версий NuGet»
Номера версий NuGet нормализуются (1.0, 1.0.0 и 1.0.0.0 — одна версия),
и две публикации одной версии сервер в одну не сводит: у них разные файлы.
Родственные отказы того же обновления — «не соответствует формату NuGet»
(версия, которую нормализовать нечем) и «содержит несколько файлов .nupkg»
(неясно, какой файл считать файлом версии).
Найдите эквивалентные версии:
SELECT p.name AS package, pv.package_id,
ARRAY_AGG(pv.version ORDER BY pv.created_at) AS variants
FROM package_versions pv
JOIN packages p ON p.id = pv.package_id
WHERE p.type = 'nuget'
GROUP BY p.name, pv.package_id,
regexp_replace(pv.version, '(\.0)+$', '')
HAVING COUNT(*) > 1;
Решите, какая публикация верна, удалите лишнюю
(DELETE FROM package_versions WHERE id = ...) и перезапустите сервер.
Запрос выше приблизителен; точную запись называет сам текст отказа, по одной
за запуск.
Сервер не стартует: «имя занято одновременно пользователем и группой»
Имя пользователя и путь группы должны различаться: одно имя, занятое обоими, сделало бы переименование одного переименованием и другого. Найдите такие имена:
SELECT u.username FROM users u JOIN groups g ON g.path = u.username;
Переименуйте одну из сторон — пользователя в админке, группу на её странице — и перезапустите сервер.
Сервер не стартует: «активации лицензий ссылаются на разные инстансы»
Признак того, что база была скопирована и обе копии активировали лицензию самостоятельно. Своими силами это не исправляется, и править базу вручную не следует: напишите издателю (info@gitriver.ru), приложите полный текст отказа и укажите, какая из копий — рабочая установка. Издатель перевыпустит лицензию, ответ вводится вручную на странице лицензии.
Если обе копии должны работать как отдельные установки, каждой нужна своя лицензия. Порядок после переезда базы или восстановления из копии — в разделе «Переезд базы и восстановление из копии».