Свой Git-сервер на VPS: установка Forgejo и Gitea

Свой Git-сервер на VPS как аналог GitHub: установка Forgejo или Gitea в Docker с PostgreSQL, SSH для git, CI-раннер, зеркала с GitHub, бэкапы и типичные грабли.

Актуально на сентябрь 2026 года: Forgejo 15.0.9 LTS и 16.0.5, forgejo-runner 13.2, Gitea 28.0. Основы Docker и nginx — в отдельных статьях, здесь — то, что ломается в эксплуатации.

Что понадобится

Шаг 1. Решите, нужен ли свой сервер и какой

Forgejo или Gitea

Forgejo — форк Gitea. В октябре 2022 года домен и товарный знак Gitea перешли новой коммерческой компании Gitea Ltd. Часть контрибьюторов просила передать их некоммерческой организации, не добилась этого и в декабре запустила Forgejo под опекой Codeberg e.V. С 2024 года это жёсткий форк, а новый код с версии 9.0 выходит под GPLv3+. Gitea осталась под MIT, платные Enterprise и Cloud продаёт CommitGo.

  • Релизы. Forgejo выходит раз в квартал: стабильная версия живёт три месяца, LTS — 15. Сейчас LTS — 15.0 (до 15 июля 2027 года), 16.0 — до 29 октября. У Gitea LTS нет, в конце сентября вышла 28.0, уже без «1.» в номере.
  • Регистрация. В Gitea 28.0 она закрыта по умолчанию, в Forgejo её закрывает установщик, а конфиг по умолчанию оставляет открытой.
  • Переезд. С Gitea до 1.22 на Forgejo переходят штатно через версию 10, с более новых — вручную или переносом репозиториев по одному.
  • CI. У Forgejo — forgejo-runner, умеющий и LXC, у Gitea — Gitea Runner, бывший act_runner.

Некоммерческое управление и LTS — Forgejo, фирменная поддержка и SAML — Gitea Enterprise. Установка ниже подходит обоим: для Gitea замените образ на docker.gitea.com/gitea, префикс переменных — на GITEA__, раннер — на Gitea Runner.

Когда свой сервер оправдан

Свой сервер нужен, когда код должен лежать на вашей инфраструктуре: закрытые проекты, требования заказчика, риск ограничений SaaS. Так, в 2025 году Organic Maps после ограничений GitHub против одного из разработчиков перенёс на свой Forgejo 54 тысячи коммитов. Если обновлять сервер и проверять бэкапы некому или проекту важна аудитория GitHub, он не нужен: открытым проектам подойдёт Codeberg.

Шаг 2. Выберите схему SSH для git

В стандартном образе Forgejo в контейнере работает OpenSSH на 22-м порту, а на хосте этот порт занят системным sshd. Схем три:

  • Отдельный порт. Порт 22 контейнера публикуется на 2222 хоста, ссылки выглядят как ssh://[email protected]:2222/team/app.git. Документация Forgejo называла этот вариант самым безопасным. Порт в адресе прячется блоком Host в ~/.ssh/config.
  • Порт 22 для git. Системный sshd переезжает на другой порт, 22 отдаётся контейнеру. Ссылки короче, но админский порт легко перепутать.
  • Проброс через sshd хоста с AuthorizedKeysCommand. Gitea и Forgejo в 2026 году убрали эти инструкции из документации: схема ломается на экранировании SSH_ORIGINAL_COMMAND, а вариант с docker exec даёт пользователю git доступ к Docker, то есть права root.

Если нужны адреса [email protected]:team/app.git без порта, ставьте Forgejo бинарником под systemd: он работает через системный sshd. Дальше — Docker и порт 2222.

Шаг 3. Поднимите Forgejo и PostgreSQL в Docker Compose

Сгенерируйте пароль базы: в hex-строке нет символов # и ;, которые в app.ini пришлось бы экранировать:

sudo mkdir -p /opt/forgejo && cd /opt/forgejo
echo "DB_PASSWORD=$(openssl rand -hex 24)" | sudo tee .env
sudo chmod 600 .env

Файл /opt/forgejo/compose.yaml:

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:15
    container_name: forgejo
    restart: always
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=${DB_PASSWORD}
      - FORGEJO__server__DOMAIN=git.example.com
      - FORGEJO__server__SSH_DOMAIN=git.example.com
      - FORGEJO__server__ROOT_URL=https://git.example.com/
      - FORGEJO__server__SSH_PORT=2222
      - FORGEJO__server__LFS_START_SERVER=true
      - FORGEJO__service__DISABLE_REGISTRATION=true
      - FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES=127.0.0.0/8,::1/128,172.16.0.0/12
      - FORGEJO__log__MODE=console,file
    volumes:
      - ./data:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "2222:22"
    depends_on:
      - db

  db:
    image: postgres:18
    container_name: forgejo-db
    restart: always
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=forgejo
    volumes:
      - ./postgres:/var/lib/postgresql
sudo docker compose up -d
sudo docker compose logs -f forgejo
  • Тег 15 — последняя 15.0.x LTS, на следующую мажорную версию он сам не переедет. Если codeberg.org недоступен, есть зеркало data.forgejo.org.
  • 127.0.0.1:3000. Опубликованные порты Docker обходят правила ufw, и без адреса веб-порт был бы открыт в интернет.
  • Переменные FORGEJO__раздел__КЛЮЧ переписывают /data/gitea/conf/app.ini при каждом старте, поэтому правка того же ключа в файле откатится.
  • SSH_PORT=2222 влияет только на ссылки. OpenSSH в контейнере слушает 22, и параметры SSH_SERVER_* к нему не относятся.
  • REVERSE_PROXY_TRUSTED_PROXIES. Прокси приходит в контейнер с адреса шлюза Docker. Образ 16.0 перестал доверять всем адресам, и без этой строки в логах будет шлюз вместо клиентов. Образ 15.0 пишет в новый конфиг * и верит X-Forwarded-For от кого угодно. Точную подсеть покажет docker network inspect forgejo_default.
  • postgres:18 монтируется в /var/lib/postgresql. С каталогом /var/lib/postgresql/data, как в примере документации с postgres:14, он не запустится, а поддержка PostgreSQL 14 кончается 12 ноября 2026 года.

PostgreSQL или SQLite

Forgejo работает с SQLite, PostgreSQL от 14, MySQL от 8.4 и MariaDB от 10.6. Документация советует SQLite при небольшой нагрузке, но одновременную запись она держит плохо. После обновления до 14.0 в issue о database is locked набралось больше 40 комментариев, блокировки давали раннеры и реестр. В другом случае очистка 20 ГБ образов на SQLite держала ядро на 100% около 50 минут. С Actions и реестром берите PostgreSQL сразу: перенос между СУБД документация называет нетривиальным.

Шаг 4. Пройдите установщик через SSH-туннель

Веб-порт слушает только 127.0.0.1, и установщик видите только вы. Не открывайте домен до конца установки: незавершённый установщик пройдёт кто угодно, а новые сертификаты публикуются в журналах Certificate Transparency, которые читают сканеры. Откройте туннель и зайдите на http://localhost:3000:

ssh -L 3000:127.0.0.1:3000 user@IP_сервера

Поля из compose уже заполнены. Галка отключения регистрации стоит по умолчанию, поэтому администратора создайте здесь же. SECRET_KEY и INTERNAL_TOKEN установщик сгенерирует сам. Из консоли администратора создаёт команда ниже, а без -u git будет ошибка Forgejo is not supposed to be run as root:

sudo docker exec -u git forgejo forgejo admin user create --admin \
  --username admin_name --email [email protected] --random-password
Важно: если пропускаете установщик через FORGEJO__security__INSTALL_LOCK=true, задайте и SECRET_KEY, сгенерировав его командой forgejo generate secret SECRET_KEY. С пустым ключом Forgejo молча берёт общеизвестное значение по умолчанию.

Шаг 5. Подключите домен и TLS

Caddy сам получит сертификат и не ограничивает размер запроса:

git.example.com {
    reverse_proxy 127.0.0.1:3000
}

Для nginx — конфиг из документации Forgejo и сертификат через sudo certbot --nginx -d git.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name git.example.com;
    merge_slashes off;
    client_max_body_size 2G;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Connection $http_connection;
        proxy_set_header Upgrade $http_upgrade;
        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;
    }
}

По умолчанию nginx принимает до 1 МБ, документация Forgejo ставит 512 МБ, но слой образа или файл LFS бывают больше, и клиент получит 413. merge_slashes off нужен для путей с закодированным слешем. Держите Forgejo в корне поддомена: реестр контейнеров всегда работает по пути /v2, для подкаталога нужны лишние правила прокси.

Шаг 6. Сделайте первую настройку

Регистрация. Оставьте её закрытой и заводите пользователей в панели администратора. Если открытая нужна, включите капчу ENABLE_CAPTCHA, подтверждение почты или ручное одобрение, для компании — EMAIL_DOMAIN_ALLOWLIST. Дело не только в спаме. В августе 2026 года на Хабре разобрали взлом Gitea 1.24.7 с открытой регистрацией: бот зарегистрировался, создал репозиторий, вызвал API diffpatch и через 11 секунд выполнил код на сервере, а потом поставил майнер (CVE-2026-60004). Forgejo с июля по сентябрь закрыл три уязвимости с выполнением кода — в 15.0.6/16.0.2, 15.0.8/16.0.4 и 15.0.9/16.0.5, и каждой хватало права создать свой репозиторий.

Закрытый просмотр. Для командного сервера включите FORGEJO__service__REQUIRE_SIGNIN_VIEW=true, и гость увидит только вход. Это же защищает от краулеров: они игнорируют robots.txt, ходят с десятков тысяч домашних IP и обходят каждый коммит — 25 ноября 2025 года на code.forgejo.org приходило около 50 тысяч адресов в час. Публичному инстансу нужен proof-of-work вроде Anubis.

Почта нужна для сброса паролей и уведомлений. Параметры внешнего SMTP добавьте в environment:

      - FORGEJO__mailer__ENABLED=true
      - FORGEJO__mailer__PROTOCOL=smtps
      - FORGEJO__mailer__SMTP_ADDR=smtp.example.com
      - FORGEJO__mailer__SMTP_PORT=465
      - [email protected]
      - FORGEJO__mailer__PASSWD=${SMTP_PASSWORD}
      - [email protected]
      - FORGEJO__service__ENABLE_NOTIFY_MAIL=true

Авторизация на SMTP работает только поверх TLS: smtps на 465 или smtp+starttls на 587. Тестовое письмо отправляется из панели администратора. Администраторам включите двухфакторную аутентификацию.

Шаг 7. Подключите Forgejo Actions и раннер

Задания Actions выполняет отдельный forgejo-runner: он сам опрашивает Forgejo по HTTPS, входящие порты не нужны. Держите его на другом VPS — CI выполняет чужой код. С меткой host изоляции нет, и задание достанет до сервисов на 127.0.0.1 мимо прокси, а с сокетом Docker (docker_host: automount) сделает docker exec в контейнер Forgejo и создаст себе администратора.

На сервере раннера подготовьте каталог и конфиг для UID 1001, под которым compose ниже запускает раннер:

sudo mkdir -p /opt/runner/data/.cache && cd /opt/runner
sudo chown -R 1001:1001 data && sudo chmod 775 data/.cache && sudo chmod g+s data/.cache
sudo docker run --rm data.forgejo.org/forgejo/runner:13 forgejo-runner generate-config | sudo tee data/runner-config.yml >/dev/null
sudo chown 1001:1001 data/runner-config.yml

Файл /opt/runner/compose.yaml по схеме из документации: задания идут во вложенном Docker и не видят Docker хоста.

services:
  docker-in-docker:
    image: docker:dind
    privileged: true
    command: ["dockerd", "-H", "tcp://0.0.0.0:2375", "--tls=false"]
    restart: unless-stopped

  runner:
    image: data.forgejo.org/forgejo/runner:13
    depends_on:
      - docker-in-docker
    environment:
      DOCKER_HOST: tcp://docker-in-docker:2375
    user: "1001:1001"
    volumes:
      - ./data:/data
    restart: unless-stopped
    command: forgejo-runner daemon --config runner-config.yml

Создайте раннер в разделе Actions → Runners на уровне сервера, организации или репозитория: чем уже область, тем меньше риск. Интерфейс выдаст UUID и токен, команда forgejo-runner register устарела. В runner-config.yml замените labels: [] в разделе runner и пустой раздел server в конце файла, затем выполните sudo docker compose up -d:

  labels:
    - docker:docker://node:lts-trixie
    - ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04

server:
  connections:
    forgejo:
      url: https://git.example.com/
      uuid: UUID_из_интерфейса
      token: ТОКЕН_из_интерфейса

Метка связывает runs-on с образом. Workflow лежат в .forgejo/workflows/, а без этого каталога Forgejo читает .github/workflows/. Минимальный пример:

on: [push, pull_request]
jobs:
  test:
    runs-on: docker
    steps:
      - uses: actions/checkout@v6
      - run: npm ci && npm test

Разработчики обещают узнаваемость, а не совместимость с GitHub Actions:

  • Окружение. Типичный образ — Debian с Node.js, у GitHub — большой образ Ubuntu, ближе к нему образы catthehacker. Скачанный образ раннер не обновляет: фиксируйте версии или включите force_pull.
  • Экшены. actions/checkout@v6 берётся с data.forgejo.org, остальные указывайте полным адресом https://github.com/owner/action@v1. Вместо actions/upload-artifact@v4 нужен https://data.forgejo.org/forgejo/upload-artifact@v4.
  • Синтаксис. Ключи permissions и continue-on-error у задания игнорируются. Раннер 13 (август 2026 года) удалил ::set-output, ::set-env и ::add-path, пишите в $GITHUB_OUTPUT, $GITHUB_ENV и $GITHUB_PATH. Ошибки в выражениях теперь валят задание, а Docker нужен версии 25 и выше.
  • Права. Pull request авторов с правом только на чтение ждут одобрения. В зеркалах и форках Actions по умолчанию выключены.

С раннером GitHub Actions из соседней статьи модель общая — исходящее подключение, метки, изоляция, — но он обслуживает только репозитории на GitHub.

Шаг 8. Настройте зеркала, пакеты и LFS

Зеркало с GitHub: «Новая миграция» → GitHub, адрес, токен (нужен для приватных репозиториев и снимает лимит API) и галка зеркала. Зеркало забирает ветки, теги, вики и по желанию LFS, но не issue и pull request — их переносит только разовая миграция без галки. Синхронизация идёт раз в 8 часов, но не чаще раза в 10 минут, а существующий репозиторий в зеркало не превратить. С 16.0 зеркала не следуют HTTP-редиректам, и зеркало переименованного на GitHub репозитория встанет.

Зеркало в GitHub настраивается в Settings → Repository → Mirror Settings с fine-grained токеном GitHub: право Contents, плюс Workflows, если есть .github/workflows. Push-зеркало делает force push и затирает изменения на GitHub, а через SSH не передаёт LFS.

Пакеты. Больше 20 форматов: npm, PyPI, Maven, Debian, Helm, generic и другие. Для Docker нужен docker login git.example.com с токеном доступа (при 2FA пароль не подойдёт), имя образа — git.example.com/team/app:1.0. Задайте правила очистки, иначе реестр будет только расти.

LFS включает строка LFS_START_SERVER=true, файлы лежат в data/git/lfs. Объекты без ссылок из коммитов не удаляются, пока выключена задача gc_lfs.

Ещё диск съедают артефакты Actions (90 дней), логи заданий (365 дней), архивы исходников, создаваемые при скачивании, и включённый индекс поиска по коду — по документации он в шесть раз больше репозиториев. Где кончилось место, покажет sudo du -sh /opt/forgejo/data/*/*, подробнее — «Закончилось место на диске».

Шаг 9. Настройте бэкапы и обновления

Бэкапы здесь делаете вы сами. База и файлы должны совпадать по времени, поэтому Forgejo на время копирования останавливают, а базу снимают pg_dump: SQL-дамп из forgejo dump документация считает ненадёжным. Скрипт /opt/forgejo/backup.sh:

#!/bin/bash
set -euo pipefail
source /etc/restic/env
cd /opt/forgejo
mkdir -p backup
docker compose stop forgejo
trap 'docker compose start forgejo' EXIT
docker compose exec -T db pg_dump -U forgejo -Fc forgejo > backup/forgejo.dump
restic backup data backup compose.yaml .env

Без -T терминал Docker испортит двоичный дамп. Файл /etc/restic/env, настройка restic и cron — в отдельных статьях, как и дампы баз. forgejo dump удобен для переезда одним архивом, но требует свободного места не меньше объёма данных.

Раз в квартал проверяйте восстановление на отдельном VPS:

  1. Укажите в compose точную версию бэкапа, например 15.0.9: более новая начнёт миграции посреди восстановления.
  2. Восстановите /opt/forgejo из restic, запустите базу (docker compose up -d db) и загрузите дамп: docker compose exec -T db pg_restore -U forgejo -d forgejo < backup/forgejo.dump.
  3. Запустите Forgejo, выполните docker exec -u git forgejo forgejo doctor check --all и проверьте клонирование по SSH и HTTPS.

После смены пути установки на 15.0 выполните forgejo admin regenerate hooks; в 16.0 хуки централизованы, и команды нет.

Обновления. Патчи ставятся после бэкапа: docker compose pull && docker compose up -d. Перед мажорной версией прочитайте несовместимые изменения в release notes, сделайте бэкап, выполните docker exec -u git forgejo forgejo manager flush-queues и смените тег. Прыгать через версии можно, а если миграция упала, пройдите по последним патчам каждой серии. О выпусках с исправлениями безопасности заранее сообщает репозиторий security-announcements на Codeberg. Мажорную версию PostgreSQL сменой тега не обновить — только дамп и восстановление.

Важно: не ставьте x.0.0 в день выхода и не обновляйтесь без бэкапа. Миграция 13.0.0 в октябре 2025 года повредила секреты Actions на PostgreSQL, где их было больше 50. В марте 2026 года обновление 13 → 14 на SQLite упало на миграции, а откатиться нельзя: Forgejo не стартует на базе более новой версии.

Шаг 10. Закройте доступ

  • Защитите хост по статьям про SSH, ufw и базовую защиту VPS. Снаружи нужны 80, 443, 2222 и админский SSH.
  • На конец сентября 2026 года держите версию не ниже 15.0.9 или 16.0.5.
  • Настройте fail2ban на неудачные входы в веб-интерфейс. Forgejo пишет строки Failed authentication attempt for имя from IP в data/gitea/log/gitea.log, если включён вывод в файл и настроены доверенные прокси. SSH контейнера пускает только по ключам.
  • Без зеркал и внешних вебхуков контейнеру не нужен выход в интернет: ограничьте его правилами DOCKER-USER. Во взломе из шага 6 майнер скачался именно так.

Фильтр /etc/fail2ban/filter.d/forgejo.local и jail /etc/fail2ban/jail.d/forgejo.local:

[Definition]
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =
[forgejo]
enabled = true
filter = forgejo
logpath = /opt/forgejo/data/gitea/log/gitea.log
port = http,https
maxretry = 10
findtime = 3600
bantime = 900

Бан на 80 и 443 работает, потому что веб-трафик приходит на прокси хоста. Если прокси в Docker, банить нужно в цепочке DOCKER-USER.

Сколько ресурсов нужно

Сам Forgejo после старта занимает 100–200 МБ, а счётчики контейнера вроде docker stats могут показывать больше, учитывая файловый кэш. Документации Gitea для небольшой команды хватает 2 ядер и 1 ГБ. Нагрузку дают сборки, индексация, архивы и краулеры: у одного пользователя сервер на 2 ГБ ушёл в своп, когда Forgejo под потоком запросов вырос до 1,1 ГБ.

  • Команда до 5 человек. Forgejo и PostgreSQL — 1–2 vCPU и 2 ГБ, NVMe от 20–40 ГБ из расчёта «репозитории × 3 плюс LFS и реестр». Раннер отдельно — 2 vCPU и 4 ГБ на типичные сборки Node.js, Python и Go. Всё на одной машине — от 4 vCPU и 8 ГБ и только для доверенного кода.
  • Команда около 50 человек. Forgejo и PostgreSQL — 4 vCPU и 8 ГБ, из них 1–2 ГБ под shared_buffers. Кэш стоит вынести в Redis или Valkey ([cache] ADAPTER = redis), поиск по коду — во внешний движок вместо встроенного bleve. Под CI — два и больше раннера на своих машинах.

В ForestsNet Cloud считается только исходящий трафик: клоны, fetch и скачивание образов с других машин. Push, зеркалирование с GitHub и загрузка базовых образов — входящий трафик, в пакет он не идёт.

Диагностика

  • Permission denied (publickey), хотя ключ добавлен. Клиент стучится в sshd хоста на 22 вместо 2222. Проверьте ssh -T -p 2222 [email protected]: Forgejo ответит Hi there, имя!.
  • REMOTE HOST IDENTIFICATION HAS CHANGED после переезда. Не восстановлен каталог data/ssh с ключами хоста.
  • В ссылках localhost или порт 3000. Не заданы DOMAIN, SSH_DOMAIN и ROOT_URL: шаблон образа пишет localhost.
  • Задание ждёт раннер. Нет раннера с меткой из runs-on или Actions выключены в репозитории, как у зеркал и форков.
  • Cannot connect to the Docker daemon внутри задания. Нужен Docker-in-Docker или docker_host с учётом рисков из шага 7.
  • Зеркало встало после перехода на 16.0. Репозиторий на GitHub переименован, а редиректам Forgejo больше не следует. Проверьте и срок токена.
  • В логах только адреса 172.x.x.x. Не настроен REVERSE_PROXY_TRUSTED_PROXIES.
  • 413 Request Entity Too Large при push. Поднимите client_max_body_size в nginx.
  • postgres:18 сразу завершается. Том должен быть смонтирован в /var/lib/postgresql.
  • Процессор на 100%, растёт repo-archive. Это краулеры: включите REQUIRE_SIGNIN_VIEW или защиту перед Forgejo. Если часами грузит индексатор кода, удалите его каталог в data/gitea/indexers/ и перезапустите Forgejo.

Источники