Свой 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 — в отдельных статьях, здесь — то, что ломается в эксплуатации.
Что понадобится
- VPS ForestsNet Cloud с Ubuntu 24.04 или Debian 12/13 и Docker Compose — см. «Docker и Docker Compose».
- Свой поддомен вроде
git.example.comс A-записью на сервер — см. «Как привязать домен к серверу». - Caddy или nginx для HTTPS — см. статьи про Caddy или nginx с Let's Encrypt.
- Вход на сервер по ключу — см. «Усиление SSH» — и место для бэкапов за пределами сервера.
Шаг 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/postgresqlsudo 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Шаг 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:
- Укажите в compose точную версию бэкапа, например
15.0.9: более новая начнёт миграции посреди восстановления. - Восстановите
/opt/forgejoиз restic, запустите базу (docker compose up -d db) и загрузите дамп:docker compose exec -T db pg_restore -U forgejo -d forgejo < backup/forgejo.dump. - Запустите 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 сменой тега не обновить — только дамп и восстановление.
Шаг 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.
Источники
- Документация Forgejo: установка в Docker, подготовка базы, рекомендуемые настройки, reverse proxy, параметры, командная строка, обновление, график релизов
- Forgejo Actions: администрирование, раннер в Docker, регистрация, метки, безопасность, Docker в заданиях, отличия от GitHub Actions, артефакты, раннер 13.0
- Forgejo: зеркала, реестр контейнеров, квоты
- Релизы и безопасность Forgejo: 16.0.5, 15.0.9, PR 12782, security-announcements
- История: анонс Forgejo, жёсткий форк, GPL, совместимость с Gitea, открытое письмо
- Обсуждения и issue Forgejo: краулеры, краулеры, ноябрь 2025, секреты в 13.0.0, миграция 13 → 14, database is locked, очистка реестра, память, индексатор bleve
- Gitea: требования, Docker, fail2ban, бэкап, Gitea 28.0.0, о Gitea Ltd, CVE-2026-60004
- Хабр: майнер через RCE в Gitea, Gitea с проверкой восстановления, корпоративный Forgejo, защита от краулеров в nginx, Organic Maps на Forgejo
- Прочее: Docker и ufw, каталог данных postgres 18, поддержка PostgreSQL