Github - отличная площадка, подарившая удаленные репозитории всем и каждому, стала центром опенсорса, упростила cicd создав удобные actions, изобрели pr без которых не обходится современная разработка. Я не собираюсь переезжать и не предлагаю это всем. Но исходники, pr, cicd и привычный git push команды зависят от сервиса со своим проблемами, над которым нет ни операционного, ни сетевого (в последнее время) контроля
А теперь к актуальному.
Апрель 2026 - баг merge queue откатил изменения в 2092 пул реквестах и исправили это недопустимым force-push. Разбор.
Декабрь 2025 - gh объявил о намерение брать деньги за self-hosted раннеры. Пользователь покупает и обслуживает сервер, и платит gh за работу своих раннеров.
За последние 2 года сервис штормит, githubstatus.com не бывает зеленым даже неделю. Хотя инженеры комании справедливо связывают проблемы с огромным наплывом комитов и реквестов из-за ai бума.
Поэтому сегодня поднимем свой git сервис на небольшой vps, подключим actions – и самое важное – сделаем бекап и проведем полное восстановление системы. Пройдемся по важным местам в безопасности, по разному создадим репозитории, настроим runner.
Ниже – весь путь. Статья рассчитана на разработчика или небольшую команду, которым нужен понятный git хостинг, а не комбайн с отдельным человеком для обслуживания.
Что получится
В результате будет простой, но взрослый контур работы с репозиториями. Не github на минималках, а полноценная работа с репозиториями, issues, pr, организациями, командами, ssh, actions, пакетами, api для доп автоматизации и полными бекапами. При этом команда контролирует данные, знает как восстановить сервис и может оставить github зеркалом проекта а не единственной точкой отказа.

Почему gitea, а не gitlab
Gitlab - сильный продукт. Но для пары репозиториев и команды из нескольких человек его возможности приносят лишнюю сложность: нужно больше мощностей, тяжелее обновления, cicd - сложный конвейер, с которым нужно учиться работать.
Gitea - компактный go бинарник с привычным интерфейсом git хостинга. Ради эксперимента я поднимал сервис на 0.25 vcpu и 512 mb памяти: несколько репозиторий и ежедневные push работали без проблем. Для статьи развернул всё на нормальной машинке.
VPS брал как обычно на petrosky.io, 1cpu и 2 gb ram за 7 европейских рубля.
Локаций сейчас две - Канада и Франция, для этого сервера взял Францию. Отдельно пригодилось то, что при создании VPS можно выбрать Gitea из списка готовых приложений. То есть вместо установки с нуля я получил стабильную автоматическую развертку приложения, после чего осталось только заняться самым приятным - настроить всё, чем мы сегодня и займемся.
По цене выходит дешевле многих российских провайдеров, плюс давно уже спокойно оплачиваю там криптой. Для git-сервера ресурсов хватает с запасом на несколько моих с командой проектов.
Перед стартом
Домен. Я добавил поддомен git.ivan-noskov.ru с А записью на ip сервера.
Где будет runner. В рамках статьи раннер развернул у себя на маке - для своих проектов эта небезопасная схема мне очень нравится - cicd летает на бешеных скоростях.
Резервная копия. Архивы резервных копий нужно хранить на независимой площадке: другая машина, объектное хранилище или на диске.
Установка gitea
В моем случае gitea установился по скрипту провайдера - он был в маркетплейсе приложений. Получился gitea 1.27.3 как systemd-сервис, mariadb и caddy для https.

Моя схема:
Gitea binary: /opt/gitea/gitea Gitea config: /opt/gitea/custom/conf/app.ini Repositories: /opt/gitea/repos Data: /opt/gitea/data Logs: /opt/gitea/log Systemd: /etc/systemd/system/gitea.service Reverse proxy: Caddy, /opt/caddy/caddyfile Database: MariaDB/MySQL SSH for Git: port 2222, user gitea
Именно этот список позже может спасти неудачное восстановление. Не переписывайте пути из чужих гайдов, сохраняйте свой вариант обязательно.
Самому gitea можно поставить через докер или бинарём.
Пользователи, орги и права: не выдаем всем админа
У gitea есть два уровня, которые лучше не смешивать:
пользователь - личная учетка человека, его ключи и персональные репозитории;
организация - общий namespace команды, например back или product. Доступом внутри управляют команды.
Пользователей я создал в админке:

Организацию создаем через + > Создать организацию. Затем добавляем в неё команды, например dev с write доступом и readers только с чтением. Так не придется выдавать права на каждый новый проект. Вообще система прав в gitea отдельно меня порадовала.

Мой минимум безопасности:
админ - отдельная учетка, а не ежедневный рабочий акк;
разрабы - права через команду организации;
двухфакторка обязательная для владельцев и админа;
регистрация выключена, доступ по vpn только для команды;
обновлять gitea и os.
Последний пункт не декоративный. Свой gitea - ещё одна поверхность атаки. https://habr.com/ru/articles/1072030 - разбор компроментации плохо защищенного сервера с установкой майнера.
Настройка двухфакторки тут, если что:

SSH: ключ добавлен, но Permission denied - ещё не диагноз
Добавим работу по ssh с репами. В профиле пользователя добавляем свой ключ Настройки > ssh/grg keys.

Gitea просит отдельно подтвердить владение ключем. Интерфейс предложит подписать одноразовый токен через ssh-keygen -y sign:
echo -n ‘{ваш токен}' \
| ssh-keygen -Y sign -n gitea -f ~/.ssh/{ваш ключик}
Вставляем то что получили и gitea теперь нам верит.
Ключ SSH «SHA256:Yw***********************j67qGzNyE» верифицирован.
Самое интересное: создаем репозиторий, мигрируем с github
Есть три сценария. Они не конкурируют, а решают разные задачи.
1. Новый проект сразу в gitea
+ > Новый репозиторий. После создания получаем готовую к работе репу.
mkdir project && cd project git init git add . git commit -m "Initial commit" git branch -M main git remote add origin ssh://gitea@{ваш домен}:2222/{ваша орга}/{ваш репо}.git git push -u origin main

2. Импорт из github
Жмем + > Новая миграция, среди разнообразия выбираем наш github и заполняем форму:

Я буду работать со своим проектом тетриса на двоих. Нагенерил как-то простую игру, чтобы можно было на одной клаве играть с девушкой: тут можно поиграть.

Для публичного репозитория достаточно ссылки, для приватного нужно будет вставить токен с gh. Перед запуском импорта, прочитайте список импортируемых сущностей. Git-история, issues, pull request'ы, вики, labels, releases и LFS зависят от источника и разрешений токена.
После переноса проверьте:
- ветки и теги;
- историю коммитов и авторов;
- LFS и большие релизные файлы;
- issues и PR, если переносили их;
- webhooks, deploy keys, Actions secrets и интеграции - не считайте их автоматически перенесёнными;
- default branch и права доступа.

3. Локальная копия уже есть: добавляем Gitea
Это мой любимый безопасный переход. Старый origin не удаляем, а переименовываем в github. Новую Gitea делаем origin:
cd ~/projects/tetris git remote rename origin github git remote add origin ssh://gitea@{ваш домен}:2222/acme/tetris.git git remote -v git fetch origin git push -u origin main
Теперь обычный git push отправляет код в Gitea, а GitHub остаётся известным remote для сверки или временного зеркала:

Это и есть спокойная миграция: публичный GitHub может остаться основой или зеркалом, но работоспособность команды больше не зависит от него как от единственного remote.
CI: подключаем Gitea Actions
Очень классная фишка проудукта - Gitea Actions совместимы с синтаксисом GitHub Actions. Сначала создадим runner. В панели Gitea получите registration token: Настройки > Действия > Runners.
Для первой проверки я использовал Mac с Docker Desktop. Данные runner'а и токен храню отдельно, файл токена доступен только владельцу:
RUNNER_HOME="$HOME/gitea-runner" mkdir -p "$RUNNER_HOME/data" chmod 700 "$RUNNER_HOME" read -s 'RUNNER_TOKEN?Вставьте токен runner: ' printf '\n' printf '%s' "$RUNNER_TOKEN" > "$RUNNER_HOME/registration-token" unset RUNNER_TOKEN chmod 600 "$RUNNER_HOME/registration-token" docker pull docker.io/gitea/runner:3 docker run -d \ --name gitea-runner \ --restart unless-stopped \ -v "$RUNNER_HOME/data:/data" \ -v "$RUNNER_HOME/registration-token:/run/secrets/registration-token:ro" \ -v /var/run/docker.sock:/var/run/docker.sock \ -e GITEA_INSTANCE_URL=https://git.example.com/ \ -e GITEA_RUNNER_REGISTRATION_TOKEN_FILE=/run/secrets/registration-token \ -e GITEA_RUNNER_NAME=runner-01 \ -e GITEA_RUNNER_LABELS=node20:docker://node:20-bookworm \ docker.io/gitea/runner:3 docker logs --tail 100 gitea-runner
У меня прошло без проблем:

У этой схемы есть важное ограничение. Монтирование /var/run/docker.sock даёт job'ам доступ к Docker-демону хоста. Такой runner допустим только для доверенных репозиториев и людей. Не подключайте его к публичным форкам и не считайте локальный runner production-исполнителем. Официальная документация Gitea разбирает этот компромисс и варианты изоляции в разделе https://docs.gitea.com/runner/installation/docker.
Первый workflow
В проект tetris я добавил .gitea/workflows/verify.yml:
name: Verify on: push: branches: [main] pull_request: permissions: contents: read jobs: verify: runs-on: node20 steps: - name: Check out repository uses: actions/checkout@v4 - name: Install dependencies run: npm ci - name: Lint run: npm run lint - name: Build run: npm run build

Первый запуск оказался красным, я не стал подгонять статью под зелёную галочку. Runner зарегистрировался, workflow запустился, зависимости установились, а CI честно остановил сборку на реальных ошибках. После их исправления зелёный запуск можно сделать условием для merge.
Бэкап - не команда, а доказанное восстановление
Для согласованной копии Gitea нужно остановить: база, файлы и Git-репозитории меняются одновременно. Автоматической команды «полностью восстановить всё» тоже нет - это контролируемая ручная процедура. Подробности есть в официальной документации https://docs.gitea.com/administration/backup-and-restore.
Я проверял свою схему: gitea + MariaDB + caddy. При другом способе установки изменятся пути и, возможно, команды, но состав копии останется тем же:
SQL-дамп базы: пользователи, настройки, issues, PR, runner'ы, метаданные.
Каталоги Gitea: repositories, data, custom/config.
Unit systemd и конфигурация reverse proxy - чтобы поднять сервис с теми же точками входа.
Контрольные суммы и независимая копия архива.
Создание резервной копии
Ниже - шаблон для моего варианта установки из бинарника. Запускать нужно от рута.
set -euo pipefail BACKUP_ROOT=/root/gitea-backups STAMP=$(date -u +%Y%m%dT%H%M%SZ) BACKUP_DIR="$BACKUP_ROOT/$STAMP" APP_INI=/opt/gitea/custom/conf/app.ini install -d -m 700 "$BACKUP_DIR" # Выпишите DB_NAME/USER/PASS/HOST/PORT из APP_INI в закрытый временный defaults-файл. # Не коммитьте этот файл и удалите его после выполнения. MYSQL_DEFAULTS=$(mktemp) chmod 600 "$MYSQL_DEFAULTS" cat > "$MYSQL_DEFAULTS" <<'EOF' [client] user=GITEA_DB_USER password=GITEA_DB_PASSWORD host=127.0.0.1 port=3306 EOF cleanup() { rm -f "$MYSQL_DEFAULTS" systemctl start gitea || true } trap cleanup EXIT systemctl stop gitea mysqldump --defaults-extra-file="$MYSQL_DEFAULTS" \ --single-transaction --routines --events --triggers \ --databases GITEA_DB_NAME | gzip -9 > "$BACKUP_DIR/database.sql.gz" tar --xattrs --acls --numeric-owner -C / -czf "$BACKUP_DIR/files.tar.gz" \ opt/gitea \ opt/caddy \ etc/systemd/system/gitea.service \ etc/systemd/system/caddy.service (cd "$BACKUP_DIR" && sha256sum database.sql.gz files.tar.gz > SHA256SUMS) gzip -t "$BACKUP_DIR/database.sql.gz" (cd "$BACKUP_DIR" && sha256sum -c SHA256SUMS) tar -tzf "$BACKUP_DIR/files.tar.gz" >/dev/null systemctl start gitea trap - EXIT rm -f "$MYSQL_DEFAULTS"
Бекап получился около 78мб: sql дамп, файловый архив и sha256sums.
Тут я получил маленький, но полезный урок. Сначала я записал контрольные суммы с абсолютными путями. На другом хосте такой манифест не проверить. Поэтому в шаблоне выше контрольные суммы создаются внутри каталога бекапа и содержит относительные имена файлов.
Бекап я поместил на ноут, в реальной эксплуатации так делать не нужно. Копия нужна на независимом носителе и с шифрованием.
Восстановление и тесты
Я сознательно тестировал на той же vps и был готов всё потерять. Не повторяйте с рабочим инстансом.
Последовательность такая:
Остановил и снёс под чистую всё на vps
Установил чистый gitea той же версии, тем же способом
Распаковал custom, data, repos и unit из бекапа, восстановил sql дамп
Запустил gitea, прошел чек-лист приёмки восстановления
set -euo pipefail RESTORE_DIR=/root/gitea-restore-from-external/20260902T145457Z (cd "$RESTORE_DIR" && sha256sum -c SHA256SUMS) systemctl stop gitea # До этого шага должны быть подтверждены RESTORE_DIR и независимая копия. # Команды удаляют данные Gitea на текущей VPS. systemctl disable gitea rm -f /etc/systemd/system/gitea.service rm -rf /opt/gitea mysql -e 'DROP DATABASE IF EXISTS gitea' # Устанавливаем проверенный бинарник той же версии, создаём пустую БД и пользователя. # Конкретные DB_NAME/USER и GRANT должны совпасть с app.ini из архива. tar --xattrs --acls --numeric-owner -xzf "$RESTORE_DIR/files.tar.gz" -C / \ opt/gitea/custom \ opt/gitea/data \ opt/gitea/repos \ etc/systemd/system/gitea.service chown -R gitea:gitea /opt/gitea/custom /opt/gitea/data /opt/gitea/repos gzip -dc "$RESTORE_DIR/database.sql.gz" | mysql systemctl daemon-reload systemctl enable --now gitea su -s /bin/sh -c '/opt/gitea/gitea admin regenerate hooks' gitea
Важные оговорки:
восстанавливать нужно на ту же версию gitea. «Заодно обновлю всё до последнего» может стать большой ошибкой;
в моём архиве сохранён
app.ini, поэтому параметры базы и SSH совпали с прежней установкой. На другом хосте осознанно заменитеROOT_URL, DNS, адреса и секреты;если меняется каталог бинарника или тип установки, перегенерируйте hooks. Иначе сайт может выглядеть живым, а
git pushначнёт падать;
Чек лист после восстановления
# Сервис и сеть systemctl is-active gitea systemctl is-active caddy ss -ltn | grep ':2222' curl -fsS -o /dev/null -w '%{http_code}\n' https://{ваш домен}/ # Данные Git и приложение find /opt/gitea/repos -type d -name '*.git' | wc -l mysql -N -e 'USE gitea; SELECT COUNT(*) FROM user; SELECT COUNT(*) FROM repository;' # Git по сети: URL нужно взять из интерфейса Gitea git ls-remote --heads ssh://gitea@{ваш домен}:2222/{ваша организация}/{ваша репа}.git main ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -T -p 2222 gitea@{ваш домен}
После восстановления gitea и caddy были активны, https вернул 200, ssh отвечал и в базе остались все пользователи и репозитории.
Что я бы добавил перед тем, как звать команду
Настроил регулярный бекап по таймеру и оповещение о неуспешном запуске.
Шифровал архив до отправки во внешнее хранилище.
Раз в квартал проводил restore drill на отдельной VPS и фиксировал фактические RTO/RPO.
Ограничил firewall: 22 для администрирования, 80/443 для веба, 2222 - только если нужен встроенный SSH Gitea.
Поднял отдельную vm под runner.
Включил защиту
main.Оставил GitHub зеркалом.
Итог
Один небольшой VPS дал нам независимый маршрут разработки: пользователи и права в организациях, Git по SSH, миграцию из GitHub без резкого обрыва, CI через Actions и проверенное восстановление.
Но самый ценный результат - не сама Gitea. Команда теперь знает, где лежат данные, как работает push, кто запускает CI, где хранится копия и как вернуть сервис после потери сервера. Когда эти ответы проверены руками, внешний Git-хостинг перестаёт быть единственной опорой и остаётся удобным инструментом, который можно использовать на своих условиях.

