3 ноября 2026 г.

Forgejo на VPS: свой Git-сервер с CI за один вечер

Поднимаем Git-сервер с CI на своём VPS: Docker Compose, раннер, HTTPS, переезд с GitHub и бэкап, восстановление которого мы проверили.

Forgejo на VPS — это ваш собственный Git-сервер: репозитории, заявки, запросы на слияние и встроенный CI, который запускает тесты при каждой отправке кода. Ниже вся цепочка от пустого сервера до работающей сборки: Docker Compose, веб-установщик, SSH-ключ, раннер, HTTPS через Caddy, переезд репозитория с GitHub и бэкап, который мы не только сделали, но и восстановили на втором экземпляре. Основную цепочку (установка, ключ, push, раннер, CI, бэкап и восстановление) мы прогнали 2026-11-03 команда за командой. Не прогоняли ssh -L для тоннеля, ssh-keygen, запуск Caddy на портах 80 и 443 и sed с https-адресом (там мы проверили близкую форму). Всё адаптированное и непроверенное помечено в тексте и в отдельном списке в конце.

Коротко. Положите в папку docker-compose.yml с образом Forgejo (веб только на 127.0.0.1, SSH на порту 222), запустите docker compose up -d, откройте веб-установщик и поставьте SSH-порт 222. Добавьте SSH-ключ в профиль и отправьте первый push. Для CI сгенерируйте общий секрет, зарегистрируйте раннер командой forgejo-cli actions register и добавьте его сервис в compose. Бэкап делает forgejo dump, но хост-ключи SSH в него не входят, а базу при восстановлении надо собирать из forgejo-db.sql, а не из файла gitea.db в архиве.

Зачем свой Git-сервер и кому он нужен

Свой сервер имеет смысл, когда код и сборку вы хотите держать у себя: приватные репозитории без оглядки на чужие условия, сборка на машине, которую вы контролируете, и возможность в любой момент всё забрать и уехать. Это работа на вас: обновления, бэкапы и безопасность теперь ваши. Если вам достаточно размещённого сервиса и возиться с этим не хочется, оставайтесь там. Если же вы разработчик, небольшая команда или агентство, которому нужно хранить клиентский код у себя, — читайте дальше.

Forgejo — программа для самостоятельного размещения Git-репозиториев: веб-интерфейс, пользователи, заявки, запросы на слияние. Это ответвление (форк) Gitea, следы заметны в строке версии (17.0.1+gitea-1.22.0) и в путях внутри образа. Forgejo Actions — встроенный CI: вы кладёте в репозиторий файл со сценарием (workflow), и при каждом push раннер запускает его в контейнере. Формат близок к GitHub Actions, но не совпадает с ним: часть чужих сценариев придётся поправить. Если вы ищете альтернативу GitHub, которую можно целиком поставить на свой сервер, это один из рабочих вариантов.

В форме заказа HIP также есть готовое приложение Forgejo. Здесь же мы ставим всё вручную, чтобы вы видели каждый шаг и сами выбирали версию.

Что получится: Forgejo на VPS и что для него нужно

К концу у вас будет Forgejo с SQLite внутри, раннер, который собирает проект в контейнере, и Caddy с HTTPS перед ними. Вот что это стоит по ресурсам. Цифры снял docker stats в простое и docker images на нашем стенде, у вас они чуть разойдутся.

Компонент

Размер образа

ОЗУ в простое

Forgejo 17.0.1

291 МБ

100-220 МиБ (замеры 116, 137, 165 и 217 МиБ в разные моменты)

Раннер v13.2.1

66,8 МБ

около 9-15 МиБ

Образ для заданий node:22-bookworm

1,64 ГБ (скачивается при первом задании)

около 3 МиБ на контейнер с простой командой

Caddy (шаг 5)

не измеряли

около 18 МиБ

В простое Forgejo и раннер вместе занимали у нас 150-230 МиБ (Forgejo 137 или 217 МиБ, раннер 9-13 МиБ). Пик суммы по всем контейнерам стенда во время CI-задания был 328 МиБ, но в сумму входили и второй экземпляр Forgejo из проверки восстановления, и Caddy, так что сам стек укладывается примерно в 230 МиБ. С запасом на систему и тяжёлые сборки закладывайте не меньше 512 МиБ свободной ОЗУ под этот стек (это наш совет, не замер). Данные после установки, двух репозиториев и трёх запусков CI заняли 29 МБ, папка раннера — 628 КБ. Вывод для выбора тарифа — наш, это не бенчмарк: для личного репозитория или небольшой команды хватит 1 vCPU и 2 ГБ ОЗУ, а для тяжёлых сборок берите запас по ОЗУ и по диску. Образ для заданий один занимает 1,64 ГБ, поэтому заодно загляните в статью о том, как чистить диск от Docker.

Понадобится:

  • VPS с Ubuntu. Мы проверяли на 24.04.5; на 22.04 не запускали.
  • Docker с Compose v2: команда docker compose version должна отвечать (у нас Docker 29.1.3 и Compose v2.40.3). Если Compose для вас в новинку, начните со статьи про несколько сервисов в Docker Compose.
  • Доступ по SSH с правами sudo.
  • Домен для шага 5. До него хватит адреса сервера и SSH-тоннеля.

Как мы проверяли: стенд — отдельный демон Docker с чистым хранилищем на VPS HIP-HOSTING, поэтому боевые контейнеры не пострадали. В выводах путь к рабочей папке заменён на ~/. Команды docker написаны без sudo: считаем, что вы root или состоите в группе docker.

Шаг 1. Forgejo в Docker Compose

Создайте папку forgejo-stack, внутри неё файл docker-compose.yml с таким содержимым. Рабочая папка важна: от её имени Compose назовёт сеть (у нас получилась forgejo-stack_forgejo), и это имя пригодится на шаге 4.

Разберём строки, которые читаются не с первого взгляда.

  • image: ...forgejo:17 — образ с тегом 17, то есть ветка 17.x. У нас под ним оказалась версия 17.0.1. Зеркало data.forgejo.org/forgejo/forgejo:17 тянется точно так же.
  • USER_UID=1000 и USER_GID=1000 — числовые номера пользователя и группы (UID и GID — так Linux различает владельцев файлов), под которыми Forgejo работает внутри контейнера. Файлы в ./forgejo будут принадлежать именно им, поэтому папка должна принадлежать тем же 1000:1000 (следующая команда).
  • restart: always — контейнер поднимется сам после падения и после перезагрузки сервера.
  • ./forgejo:/data — всё, что надо сохранить (репозитории, база, конфиг), лежит в обычной папке рядом с compose. Контейнер можно пересоздать, данные останутся.
  • 127.0.0.1:3000:3000 — веб-интерфейс слушает только сам сервер. Снаружи его не видно, и это сознательно: пока перед ним нет HTTPS, форму входа в интернет выставлять нельзя. Потом перед ним встанет Caddy.
  • 222:22 — SSH Forgejo внутри контейнера слушает 22, а наружу мы отдаём его на 222: порт 22 уже занят системным SSH, через который вы заходите на сам сервер. Если оставить так, как написано, порт слушается на всех адресах сервера (разбор на шаге 5).

Создаём папку данных с нужным владельцем и запускаем. Без chown у Forgejo не будет прав писать в папку, которую до этого создали вы.

Docker скачает образ (291 МБ). Сразу после запуска веб отвечает кодом HTTP 200. Проверим, что контейнер жив и порты такие, как мы хотели:

Здесь важна колонка PORTS: веб привязан к 127.0.0.1, а 222 — к 0.0.0.0 и [::], то есть он виден всему интернету, если ему не мешает файрвол. К этому вернёмся. Время в колонке STATUS у вас будет своим.

Шаг 2. Веб-установщик Forgejo

Веб ещё закрыт для интернета, поэтому на этом этапе заходим через SSH-тоннель: это когда порт сервера пробрасывается на ваш компьютер по SSH-соединению. С ноутбука выполните ssh -L 13000:127.0.0.1:3000 user@IP-сервера и откройте в браузере http://127.0.0.1:13000. Подробно про тоннели — в статье про проброс портов по SSH. Поэтому на всех скриншотах ниже в адресе стоит 127.0.0.1:13000, а не домен.

Откроется страница «Initial configuration». Большинство полей трогать не надо: значения по умолчанию подходят.

Поле

Что делать

Почему

Database type / Path

оставить SQLite3 и /data/gitea/gitea.db

Проверяли только этот вариант. PostgreSQL и MySQL мы не пробовали

Repository root path, User to run as

оставить /data/git/repositories и git

Совпадает с устройством образа

Server domain

домен или адрес сервера (на стенде 127.0.0.1)

Попадёт в адреса клонирования

SSH server port

поставить 222

Это порт, который увидят клиенты. Внутри контейнера SSH по-прежнему слушает 22

HTTP listen port

оставить 3000

Совпадает с портом в compose

Base URL

адрес, по которому вы открываете сайт (на стенде http://127.0.0.1:13000/)

Позже заменим на https-адрес домена (шаг 5)

Disable self-registration

оставить включённой

Иначе любой, кто найдёт адрес, заведёт аккаунт. При работающем раннере это особенно неприятно (см. безопасность раннера)

Enable update checker

включена по умолчанию, мы не трогали

Раз в какое-то время смотрит TXT-запись DNS release.forgejo.org и сообщает о новой версии

Administrator account settings

раскрыть и создать администратора

Блок свёрнут по умолчанию. Без него вы потом не войдёте

Установщик с заполненными полями. Адрес в браузере выше кадра: 127.0.0.1:13000 через SSH-тоннель.

Мы создали администратора fjadmin с адресом [email protected]; у вас должны быть настоящий адрес и длинный пароль. Нажмите «Install Forgejo» и подождите: у нас до панели прошло около 20 секунд. После установки в forgejo/gitea/conf/app.ini появляются ROOT_URL, SSH_DOMAIN, SSH_PORT = 222 и INSTALL_LOCK = true, этот файл нам ещё понадобится.

Панель после установки. Внизу страницы видна версия: 17.0.1.

Версию можно спросить и через API, пригодится после обновлений. Работает только после установки: до неё API отвечает Not Found.

Шаг 3. SSH-ключ и первый push

Ключ нужен, чтобы отправлять код без пароля: у вас остаётся закрытая половина, а в Forgejo вы кладёте открытую. Если пары ключей нет, создайте её командой ssh-keygen -t ed25519 и задайте парольную фразу. Открытый ключ — это содержимое файла с расширением .pub. Добавьте его в профиль: Settings → SSH / GPG keys → Add key.

Раздел SSH / GPG keys. Ключ lab добавлен, Forgejo показывает его отпечаток и дату последнего использования.

Теперь проверьте, что сервер вас узнаёт. Мы запускали команды на самом сервере, поэтому адрес 127.0.0.1; с вашего компьютера подставьте адрес сервера или домен, но тогда порт 222 должен быть до него доступен (см. шаг 5). При первом подключении SSH спросит, доверять ли ключу сервера, — это нормально.

  • -T — не открывать терминал: Forgejo всё равно не даёт оболочку.
  • -p 222 — порт, который мы отдали наружу.
  • Пользователь всегда git, это не ваше имя: Forgejo различает людей по ключу.

Ответ «Hi there, ...» — успех. Дальше создайте пустой репозиторий demo без README (кнопка «+» в шапке → New repository; мы создавали через API) и отправьте в него код. Репозиторий заводите в отдельной папке, например ~/demo, а не в forgejo-stack: иначе README и коммиты попадут в папку самого стека. Команды docker compose выполняйте из forgejo-stack.

Среди строк вывода ищите * [new branch] main -> main. Порт 222 в адресе обязателен: без него git пойдёт на 22 и попадёт в системный SSH.

Шаг 4. Раннер Actions: свой CI

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

Регистрация: общий секрет вместо токена

В старых статьях раннер регистрируют командой forgejo-runner register с токеном из веб-интерфейса. В документации Forgejo команда forgejo-runner register помечена как устаревшая (deprecated). Мы на v13.2.1 пошли путём с общим секретом, как в официальном примере compose: вы придумываете общий секрет, говорите Forgejo «такой раннер существует», а раннеру вписываете тот же секрет в конфиг. Токен регистрации из веб-интерфейса в роли секрета не подходит, это прямо сказано в том же примере. Секрет — 40 шестнадцатеричных символов, а UUID раннера строится из первых 16 символов этого секрета.

Выполняйте всё в одном окне терминала: переменные SECRET и UUID нужны и здесь, и в конфиге ниже.

  • openssl rand -hex 20 — 20 случайных байт в виде 40 шестнадцатеричных символов.
  • -u git — команда выполняется от пользователя git, под которым живёт Forgejo.
  • --name lab-runner — имя раннера в списке; --keep-labels — при повторной регистрации не затирать метки, уже записанные в Forgejo; сами метки задаются в конфиге раннера (ниже).

Вывод последней команды мы не сохраняли; проверка успеха будет в админке и в логе раннера ниже. Секрет нигде не публикуйте.

Конфиг раннера

Теперь файл настроек раннера. Создайте runner/config.yml в любом редакторе с содержимым ниже. В карточке значения uuid и token скрыты звёздочками: впишите туда то, что напечатают echo "$UUID" и echo "$SECRET".

  • capacity: 1 — раннер берёт одно задание за раз; на небольшом сервере это защищает память.
  • timeout: 3h — задание, которое идёт дольше трёх часов, будет остановлено.
  • labels: docker:docker://... — метка docker и образ, в котором пойдут задания. В сценарии вы напишете runs-on: docker, и задание попадёт сюда. Образ скачается при первом запуске.
  • container.network — сеть Compose, в которой живёт Forgejo. Без неё контейнер задания не увидит адрес http://forgejo:3000. Имя сети — это имя папки плюс _forgejo; на своём сервере проверьте через docker network ls.
  • docker_host: "-" — не отдавать сокет Docker в контейнеры самих заданий (по документации Forgejo это значение по умолчанию). Нужен Docker внутри заданий — читайте раздел документации про доступ к Docker.
  • Права файла: он содержит секрет, читать его должен только владелец. Выставим их командой ниже.
$ sudo chown 1000:1000 runner/config.yml && sudo chmod 600 runner/config.yml

Сервис раннера в compose

Раннеру нужен доступ к Docker сервера, иначе ему нечем запускать контейнеры заданий. Мы пробрасываем в него сокет Docker. Сначала узнайте номер группы, которой принадлежит сокет: пользователь раннера должен в неё входить.

У нас это 112, у вас может быть другое число. Допишите сервис runner в конец docker-compose.yml, подставив своё число в group_add. Вот файл целиком, как он выглядит после правки (первый сервис вы уже знаете):

Для раннера тот же принцип, что и у сервера: данные в ./runner, общая сеть, перезапуск при падении, depends_on запускает его после контейнера Forgejo (готовности Forgejo он не ждёт). Строка с сокетом пробрасывает Docker сервера внутрь контейнера раннера, а group_add даёт пользователю раннера право его читать. Запустим и посмотрим:

В логе раннера должно появиться, что он зарегистрирован: у нас это строка runner: lab-runner, with version: v13.2.1, with labels: [docker], ephemeral: false, declared successfully, а следом [poller] launched. В веб-интерфейсе раннер виден в админке (Admin settings → Actions → Runners): имя lab-runner, метка docker, тип Global, статус Idle.

Раннер зарегистрирован и ждёт заданий. Рядом есть кнопка «Create new runner»: этим путём мы не шли.

Первый сценарий и результат

Сценарии лежат в репозитории в папке .forgejo/workflows. Положим туда самый простой: он забирает код и печатает версию Node, имя машины и список файлов. Так мы увидим, что вся цепочка (push, раннер, контейнер) работает.

  • on: [push] — запускать при каждой отправке коммитов.
  • runs-on: docker — метка раннера из конфига.
  • actions/checkout@v4 — готовый шаг, который скачивает код репозитория в контейнер. Forgejo берёт его с https://data.forgejo.org/actions/checkout.
  • name: — подпись шага в интерфейсе. Без неё многострочный run: | Forgejo показывает в заголовке одной длинной строкой, поэтому давайте шагам имена.

Закоммитьте файл и отправьте. Раннер подхватит задание сам, через несколько секунд оно появится в репозитории на вкладке Actions.

Эти строки — лог раннера, когда он уже принимал задания; номер задачи у вас будет свой. На нашем стенде первый запуск повис на «Set up job» минуты на две с половиной и упал, поэтому в списке на скриншоте красный запуск №1. Причина была в стенде, а не в Forgejo (подробнее ниже), после исправления остальные запуски прошли успешно, последний, №4, за 2 секунды: образ к тому времени уже лежал на диске. На самом первом запуске у вас добавятся минуты на загрузку 1,64 ГБ.

Список запусков: №1 упал на зависании сети стенда, №2-4 успешны.

Лог запуска №4: все шаги раскрыты, у каждого своё имя из name:.

В логе видно: v22.23.4 (версия Node в образе), строку hello from the runner on с именем контейнера и список файлов, среди которых .forgejo, .git и README.md (на скриншоте есть ещё restored.txt из проверки восстановления).

Безопасность раннера

Раннер по определению выполняет произвольный код: всё, что написано в сценарии, он исполняет у вас на сервере. Это называют удалённым выполнением кода (RCE), и здесь это не уязвимость, а принцип работы. Сокет Docker смонтирован в контейнер раннера: он создаёт контейнеры заданий, то есть управляет демоном хоста. В нашем конфиге стоит docker_host: "-", и по документации Forgejo это значит, что в контейнеры самих заданий сокет не отдаётся (это значение по умолчанию). Режим automount, который отдаёт сокет заданиям, по документации «не даёт никакой изоляции»: из задания можно, например, смонтировать корень хоста. У нас он не включён. Но и без него раннер выполняет произвольный код (в документации сказано, что вся цель Forgejo Actions — удалённое выполнение кода), поэтому схема допустима только для доверенных пользователей и своих репозиториев. Не включайте открытую регистрацию и не пускайте на этот раннер чужие репозитории.

Вариант

Когда подходит

Статус у нас

Сокет Docker хоста только в контейнере раннера, docker_host: "-" (как в статье)

Свой код и свои люди

Проверено, работает

automount: сокет отдаётся и заданиям

Только если вы понимаете риск: по документации изоляции нет

Не включали

Docker-in-Docker (DinD)

Когда нужна изоляция от демона хоста; описан в документации Forgejo (раздел про доступ к Docker)

Не проверяли

Отдельная виртуальная машина или сервер под раннер

Когда в репозиториях может быть чужой код (документация советует изолировать раннер в своей виртуальной машине)

Не проверяли

Если «Set up job» висит

Скорее всего, контейнер задания не выходит в интернет и не может скачать образ или шаг. Проверьте это напрямую, без Forgejo:

$ docker run --rm alpine wget -qO- https://data.forgejo.org

Если команда зависла или вернула сетевую ошибку, дело в сети Docker или файрвола. У нас так было из-за стенда: политика пересылки в UFW и отдельный Docker без своих правил; на обычном сервере с Docker из пакета такого, как правило, не бывает, но отдельно мы это не проверяли. Вторая частая причина — забытый container.network в конфиге: тогда задание не видит http://forgejo:3000.

Шаг 5. Домен и HTTPS через Caddy

Caddy — веб-сервер, который умеет сам получать и продлевать сертификаты. Здесь он работает как обратный прокси: принимает HTTPS-запросы на вашем домене и передаёт их Forgejo. Установку Caddy на хост мы разбирали в статье про Caddy; тут запустим его в контейнере в той же сети, чтобы он видел Forgejo по имени.

Сначала домен: создайте A-запись на IP вашего сервера (как это делается, написано в справке про привязку домена). Затем Caddyfile из двух строк по существу:

git.example.com замените своим доменом. forgejo:3000 — имя контейнера и его порт внутри общей сети: поэтому порт веба на хосте нам по-прежнему не нужен, 127.0.0.1:3000 можно оставить как есть. Запускаем:

$ docker run -d --name caddy --restart unless-stopped --network forgejo-stack_forgejo -p 80:80 -p 443:443 -v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" -v caddy_data:/data caddy:2
  • --network forgejo-stack_forgejo — та же сеть, что у Forgejo.
  • -p 80:80 -p 443:443 — порты, на которых Caddy общается с интернетом и получает сертификат.
  • -v caddy_data:/data — том, где Caddy хранит сертификаты, чтобы не просить их заново после пересоздания контейнера (так рекомендует документация Caddy).

Порты 80 и 443 должны быть открыты в файрволе (например, в UFW), а A-запись уже указывать на сервер, иначе сертификат не выдастся.

Честно о проверке. Настоящий сертификат для настоящего домена мы не выпускали: на стенде нет домена и открытых 80/443. Что делает Caddy на настоящем домене (сам берёт сертификат, обычно у Let's Encrypt, с запасным вариантом, и продлевает его), мы описываем по документации Caddy. Мы проверили всё остальное: связку Caddy и Forgejo с локальным сертификатом. Для этого в Caddyfile стояли тестовое имя git.lab.test и строка tls internal, а сам Caddy слушал порт 18443 на localhost.

Ответ API через Caddy пришёл с кодом 200. Флаг -k нужен только на стенде: он отключает проверку сертификата, потому что tls internal выдаёт локальный. На реальном домене -k не нужен и tls internal не пишите.

Теперь скажем самому Forgejo, что он живёт на https-адресе: ROOT_URL в forgejo/gitea/conf/app.ini должен совпадать с публичным адресом, иначе ссылки клонирования будут неправильными. Мы так же меняли ROOT_URL и SSH_PORT через sed при восстановлении, и это сработало. Рядом в файле лежит и SSH_DOMAIN, посмотрите его тоже.

Порт 222: открыть или закрыть

Помните колонку PORTS? Порт 222 слушает docker-proxy на всех адресах (0.0.0.0:222 и [::]:222). На нашем стенде UFW пропускал только 22, поэтому снаружи 222 не был виден. На сервере без файрвола он доступен всем, и решать надо осознанно. Проверьте с другого компьютера, а не на слово.

  • Нужен git push по SSH снаружи — оставьте 222 и откройте его в UFW сами.
  • Хватает HTTPS-клонирования — закройте порт: в compose замените '222:22' на '127.0.0.1:222:22', как мы сделали для веба.

Шаг 6. Переезд с GitHub

Forgejo умеет забрать репозиторий из другого места: кнопка «+» в шапке → New migration, затем выбор источника. Мы выбрали обычный Git (форма «Migrate from Git»): она копирует только сам репозиторий по ссылке на клон. Перенос issues и запросов на слияние идёт через плитку конкретного сервиса, для GitHub там нужен токен; этого мы не проверяли.

Форма переноса. Для публичного репозитория достаточно адреса и имени.

Мы проверили это на публичном учебном репозитории octocat/Hello-World: вызвали API миграции с service=git и mirror=false, то есть обычная копия, а не зеркало. Получили репозиторий fjadmin/hello-world: ветка по умолчанию master, три ветки, история на месте (7fd1a60 Merge pull request #6 ..., 7629413, 553c207 first commit).

Перенесённый репозиторий: коммиты и ветки на месте.

Чего мы не проверяли: перенос issues, запросов на слияние и wiki (для него нужен токен доступа в исходном сервисе), зеркало с автообновлением, приватные репозитории и LFS. Не обещаем, что это сработает так же. Начните с одного неважного репозитория и сравните результат с оригиналом.

Шаг 7. Бэкап и восстановление, которому можно верить

Бэкап, который никогда не восстанавливали, — это надежда. Мы сделали дамп, развернули его на новом экземпляре рядом и нашли четыре ловушки. Одна из них (хост-ключи SSH) упомянута в официальной документации по бэкапу, но легко теряется из виду; остальные три мы нашли на стенде сами.

Как делать бэкап

Команда forgejo dump собирает один zip-архив: репозитории, базу, настройки. Запускаем от пользователя git и пишем в /tmp внутри контейнера, затем копируем архив на хост:

  • -c ...app.ini — какой конфиг брать за основу.
  • -f ... — куда записать архив.
  • Успех — строка Finished dumping in file. У нас архив с двумя репозиториями (один из них перенесённый Hello-World) занял 22 МБ.

Внутри архива: app.ini, data/ (в ней gitea.db, conf, jwt, actions_log и другое), forgejo-db.sql и repos/. Относитесь к нему как к паролю: в нём конфиг, секреты и все ваши приватные репозитории. И не оставляйте на том же сервере: бэкап рядом с данными пропадает вместе с ними. Копируйте его на другую машину или в S3-совместимое хранилище, а как организовать это на уровне всего сервера, описано в справке о бэкапе сервера.

Что

В дампе?

Что делать

Репозитории, пользователи, заявки, история Actions

да (repos/, forgejo-db.sql, data/)

Ничего особенного

app.ini

да

Поправить адреса, если переезжаете

Хост-ключи SSH сервера (forgejo/ssh)

нет

Копировать отдельно (ловушка 1)

docker-compose.yml, runner/config.yml, Caddyfile

нет, в составе архива их нет

Хранить вместе с бэкапом, но помните, что в конфиге раннера секрет

Как восстанавливать

Ниже порядок, который у нас сработал. Мы разворачивали дамп в новую папку рядом с живым экземпляром (порты 3001 и 223, имя контейнера forgejo-restore), поэтому адреса в app.ini меняли. При восстановлении на том же адресе, что и раньше, менять их не надо.

Сначала распакуем архив и разложим файлы по местам, где Forgejo их ждёт. Папка repos/ становится git/repositories/, содержимое data/ попадает в gitea/. В адресе 13001 — порт SSH-тоннеля до порта 3001, как 13000 до 3000 в шаге 2.

Дальше два шага, на которых и лежат ловушки. Папка .ssh должна существовать заранее, с владельцем 1000 и правами 700. А базу мы собираем заново из текстового дампа: удаляем файл gitea.db, который приехал в архиве, и создаём новый из forgejo-db.sql с помощью sqlite3, запущенного в том же образе, чтобы ничего не ставить на сервер.

Команда с sqlite3 молчит, если всё в порядке. Теперь запускаем и сразу пересоздаём серверные хуки: это маленькие скрипты внутри репозиториев, которые связывают push с Forgejo, и после переноса путей их надо обновить.

Четыре ловушки

1. Хост-ключи SSH в дамп не входят. Это ключи самого сервера, по ним клиенты узнают, что подключаются туда же, куда и раньше. Новый экземпляр сгенерирует свои, и у всех пользователей появится ошибка «REMOTE HOST IDENTIFICATION HAS CHANGED». Папку forgejo/ssh со старого сервера надо копировать отдельно. Вот отпечатки на нашем стенде: сначала у старого (порт 222) и нового (223), потом после копирования.

Мы копировали папку уже после первого старта, остановив контейнер, и отпечатки совпали. Логичнее положить её до первого запуска, но такой порядок мы отдельно не гоняли. Главное — делайте копию forgejo/ssh вместе с каждым дампом.

2. Не берите data/gitea.db из архива как базу. Копия в архиве оказалась без части последних записей. Скорее всего, они ещё лежали в журнале WAL (SQLite применяет его позже): дамп мы снимали на работающем Forgejo, а официальная документация вообще советует останавливать Forgejo на время бэкапа. Мы взяли эту копию и получили историю Actions 1=running вместо настоящей 2=success 1=failure. Поэтому в рецепте выше базу собирает sqlite3 из forgejo-db.sql.

3. Папка forgejo/git/.ssh должна существовать заранее, с владельцем 1000 и правами 700. Иначе при первом старте на готовой базе контейнер падает с ошибкой уровня [F] (фатальная): inspect authorized_keys failed: open /data/git/.ssh/authorized_keys: permission denied. Причина в том, что entrypoint образа создаёт .ssh от имени root.

4. Не запускайте forgejo admin regenerate keys через docker compose exec. У нас эта команда записала в authorized_keys строку с путём /usr/local/bin/forgejo, а при следующем старте контейнер отказался подниматься: Key on line 2 of /data/git/.ssh/authorized_keys does not exist in database. В логе заодно были подсказки: удалить файл (Forgejo создаст его заново) или выставить SSH_ALLOW_UNEXPECTED_AUTHORIZED_KEYS=true. Без этой команды Forgejo сам создаёт authorized_keys со строкой вида command="/usr/local/bin/gitea --config=/data/gitea/conf/app.ini serv key-1",..., и SSH работает. По документации, при необходимости файл обновляют cron-задачей в админке (Admin → Monitor → Cron); эту задачу мы не проверяли. regenerate hooks проблем не вызывал.

Что показала проверка

Мы проверили восстановленный экземпляр по пунктам. Вход администратором работает, репозиторий fjadmin/demo на месте, коммиты тоже. История Actions полная: 2=success 1=failure, а не 1=running, как при копировании готовой базы. Так ответил API восстановленного экземпляра (разбирали ответ маленьким скриптом):

  • пользователь: fjadmin, администратор;
  • репозиторий: fjadmin/demo, SSH-адрес ssh://[email protected]:223/fjadmin/demo.git;
  • коммиты: 6412cca second run after network fix и f04b88f first commit with CI;
  • история Actions: runs: 2=success 1=failure.

git clone по HTTP отработал. После переноса хост-ключей работает и SSH: связка из authorized_keys и хуков живая, push проходит.

Затем мы сделали новый коммит и отправили его в восстановленный экземпляр. API показал последним коммит 3cd3586 commit after restore. Это и есть проверка: бэкап не просто распаковался, в нём можно работать.

Ещё одна проверка для тех, у кого есть CI. Мы прогнали весь путь заново на чистой папке, выполняя команды из этой статьи по порядку, а на «новом сервере» положили рядом с восстановленными данными папку runner и compose-файл с раннером. Раннер вернулся с записью declared successfully без новой регистрации, подхватил коммит, отправленный после восстановления, и завершил задание как success. Регистрация раннера едет вместе с базой, а вот его config.yml и секрет в нём не едут, поэтому папку держите в своём бэкапе.

Как обновлять Forgejo

Обновление между версиями мы на стенде не проверяли, ниже схема по логике Docker Compose и документации, не наш результат. Порядок осторожный:

  1. Сделайте дамп, как выше, и скопируйте его с сервера. Это точка, куда можно вернуться.
  2. Прочитайте заметки к выпуску на сайте Forgejo: при смене основной версии там бывают шаги, которых нет в этой статье.
  3. Закрепите тег. В нашем compose стоит :17 — это «любая 17.x», она сама сдвинется при следующем pull. Тег 17.0 держится на последнем патче 17.0.x, а 17 — на последнем миноре 17.x. Для предсказуемости укажите нужный тег (какая версия у вас сейчас, видно в подвале страницы или в ответе /api/v1/version) и на 18 переходите вручную, по официальному руководству по обновлению.
  4. Выполните docker compose pull, затем docker compose up -d. Проверьте версию тем же curl и зайдите в панель.
  5. После запуска выполните docker compose exec -u git server forgejo doctor check --all: так велит официальное руководство, мы эту команду не запускали. Руководство: forgejo.org/docs/latest/admin/upgrade/.

Что мы не проверяли

Чтобы вы не удивились на своём сервере, вот честный список. Всё из него — территория, где вам придётся сверяться с документацией Forgejo.

Проверено на стенде

Не проверялось

Ubuntu 24.04.5, Docker 29.1.3, Compose v2.40.3

Ubuntu 22.04, ARM, Windows-раннер

Forgejo 17.0.1 с SQLite

PostgreSQL и MySQL, rootless-образ, другие версии, обновление между версиями

Раннер v13.2.1 через общий секрет и сокет Docker

Путь «Create new runner», режим automount, DinD

Caddy с tls internal

Настоящий HTTPS-сертификат на домене

Миграция публичного репозитория с историей и ветками

Issues, запросы на слияние, wiki, зеркала, приватные репозитории, LFS

forgejo dump и восстановление на новый экземпляр

Бэкап варианта с PostgreSQL, холодный бэкап копией папки, восстановление на другую версию, дамп с LFS и пакетами

—

Почта (SMTP), реестр пакетов, готовое приложение Forgejo в форме заказа HIP

Частые вопросы

Сколько ОЗУ нужно Forgejo на VPS?

В простое сам Forgejo занимал у нас от 100 до 220 МиБ, раннер около 9-15 МиБ; вместе они держались в пределах 150-230 МиБ, а пик по всем контейнерам стенда во время задания составил 328 МиБ (с учётом второго экземпляра Forgejo и Caddy). Для личного репозитория или небольшой команды мы считаем достаточным 1 vCPU и 2 ГБ ОЗУ; это вывод из замеров, а не бенчмарк, а для тяжёлых сборок нужен запас по памяти и диску.

Можно ли перенести с GitHub заявки и запросы на слияние?

Мы проверили только перенос самого репозитория с историей и ветками. Обычная форма «Migrate from Git» копирует только репозиторий. Перенос issues, запросов на слияние и wiki идёт через плитку конкретного сервиса и требует токена доступа в нём; мы этого не пробовали. Не рассчитывайте на него, пока не проверите на пробном репозитории.

Безопасно ли отдавать раннеру сокет Docker?

Только для доверенного кода. Сокет смонтирован в контейнер раннера, он управляет демоном хоста; при docker_host: "-" (как у нас) в контейнеры заданий сокет не попадает. Режим automount, который отдаёт его и заданиям, по документации не даёт никакой изоляции. В любом случае раннер выполняет всё, что написано в сценариях. Для чужих репозиториев документация предлагает изолировать раннер в отдельной виртуальной машине или использовать Docker-in-Docker; мы эти варианты не проверяли.

Как сделать бэкап Forgejo и ему доверять?

Командой forgejo dump, затем скопируйте архив с сервера. Отдельно сохраните папку forgejo/ssh с хост-ключами, потому что в дамп она не входит. И один раз восстановите архив на пробном экземпляре: базу надо собирать из forgejo-db.sql, а не брать gitea.db из архива.

Что делать, если задание зависло на «Set up job»?

Проверьте, что контейнер выходит в интернет: docker run --rm alpine wget -qO- https://data.forgejo.org. Если команда не отвечает, ищите проблему в сети Docker или файрволе. Заодно убедитесь, что в конфиге раннера указана сеть Compose в container.network.

Нужен ли домен и почему SSH на порту 222?

Для первой установки домен не нужен: хватит SSH-тоннеля на 127.0.0.1. Для HTTPS домен нужен, на нём Caddy получает сертификат. Порт 222 выбран потому, что 22 занят системным SSH сервера; по нему вы входите на сам сервер, а не в Forgejo.

Коротко

  • Forgejo и раннер вместе занимали у нас 150-230 МиБ ОЗУ в простое (пик стенда во время задания — 328 МиБ с учётом второго экземпляра и Caddy); образ для заданий весит 1,64 ГБ и скачивается при первом запуске.
  • Веб держите на 127.0.0.1:3000, а наружу выставляйте только через Caddy с HTTPS. SSH-порт 222 осознанно откройте или закройте.
  • Раннер мы регистрировали общим секретом (forgejo-cli actions register), а не устаревшей командой forgejo-runner register.
  • Раннер запускает любой код из сценариев, а его контейнер управляет Docker хоста: только свои репозитории и свои люди. С docker_host: "-" сокет не попадает в сами задания, режим automount мы не включали.
  • Дамп не содержит хост-ключей SSH. Копируйте forgejo/ssh отдельно, иначе у клиентов появится предупреждение о смене ключа.
  • При восстановлении собирайте базу из forgejo-db.sql, заранее создайте .ssh (владелец 1000, права 700) и не запускайте regenerate keys через docker compose exec.
  • Настоящий HTTPS на домене, обновление между версиями и перенос issues и запросов на слияние мы не проверяли.

Итог

За вечер реально пройти всю цепочку: compose, установщик, ключ, раннер и первая зелёная сборка. Самое ценное в результате — не сам Forgejo, а уверенность в бэкапе: вы знаете, что архив разворачивается, и знаете четыре места, где он обычно ломается. Если нужен сервер под такой стек, подойдёт VPS в HIP-HOSTING: заказывайте его под нагрузку, которую показывают замеры выше. Перед установкой не забудьте о базовой защите сервера, а после — поставьте бэкап на расписание.

PUBLISHED
3 ноября 2026 г.
AUTHOR
HIP-HOSTING
LANGUAGES
EN · RU