用 Forgejo 自建 Git 仓库
用 Forgejo 自建私人 Git 服务,测试普通账号 clone、push 和私有权限。备份整份应用状态,不能只 clone 仓库。
想把私人代码、配置和维护脚本放在自己的 Git 服务里,可以用 Forgejo。账号、访问权限、备份和升级也要自己维护。如果只是想异地保存代码,已有托管仓库可能更省事。
这篇建立一个通过 HTTPS 使用的私人实例,不启用额外的 Git SSH 入口,也不改服务器原有 SSH 端口。先在转发中完成初始化,再开放域名。
先确认磁盘保存哪些东西
仓库 Git 对象只是资料的一部分。用户、议题、权限、附件、LFS 与配置也可能需要恢复。为小型个人实例选配置时,按代码体积、附件增长和其他容器总负载安排;CI 构建要另外计算,不能按 Git 服务空闲时的用量选配置。
已有 Linux VPS、Docker Engine 与 Compose 插件时可以先试用。没有可用机器,先看月付 VPS 的试用与续费条件,用小项目验证恢复后再决定长期配置。
小型个人 Git 服务可以从 2 核、2 GB 内存的 Linux 实例试用;这是选型起点,不是本次测得的最低配置。代码、LFS 和附件较多时按增长配盘;CI 构建另算内存与 CPU。可比较 Evoxt 的私有 Git 实例与地区,需要中文管理资料可看 雨云 RCS 的 Linux 配置。先验证普通账户 push、私有权限和整份数据恢复,再增加构建任务或延长账期。
创建数据目录,下面的 Linux Docker Engine 示例固定官方 Forgejo 16.0.6:
sudo mkdir -p /opt/forgejo/data
sudo chown 1000:1000 /opt/forgejo/data
cd /opt/forgejo
git.example.com 替换为自己的域名。创建 compose.yaml:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:16.0.6@sha256:e2c34a699543ca8a00217ee9ab2bea5feedc98bd6d0ea95ec287d3b7ae1355c6
restart: unless-stopped
ports:
- "127.0.0.1:13000:3000"
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__server__DOMAIN: git.example.com
FORGEJO__server__ROOT_URL: https://git.example.com/
FORGEJO__server__DISABLE_SSH: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
volumes:
- ./data:/data
这里的摘要是该发布的多平台索引,Docker 会选择对应架构;本次恢复演练只运行了 ARM64 镜像,不能据此说 AMD64 已实测。更换 UID、GID 时也要调整数据所有者。跨主要版本升级先检查迁移说明和支持周期,不要只改标签;同版本恢复也不能代替升级测试。
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose logs --tail=100 forgejo
从自己的电脑转发:
ssh -N -L 127.0.0.1:13000:127.0.0.1:13000 user@YOUR_VPS_IP
打开 http://127.0.0.1:13000 完成安装。小型新实例可以选择 SQLite,确认数据库和仓库位于 /data 的持久化范围,创建首个管理员并保持公开注册关闭。域名重定向可能要求切换到正式入口,应先配置 HTTPS 再完成依赖域名的操作。
用普通账号测试 clone 和 push
用现有 HTTPS 网关代理到服务器本机 13000。网关在容器中时改用共同网络与服务名,不能把它的 127.0.0.1 当成宿主机。确认根地址仍是实际 HTTPS URL,然后重新登录。
创建一个私有测试仓库。用普通账户按需要授予权限,以客户端支持的凭据方式配置令牌,避免写到带密码的 URL、终端历史或公开仓库。先 clone,再提交一份无敏感内容的文件并 push,最后从另一个空目录重新 clone,核对新提交。
另用未登录窗口访问私有仓库,确认内容不可读。管理员能 push 不能代替普通账号的权限检查;公网 Git SSH 已禁用,也不能直接照界面里的 SSH 命令尝试。确实需要 Git SSH 时,应另行规划专用端口、认证和防火墙,再开启对应功能。
外部 webhook、邮件和自动构建均是额外配置,首页打开不能证明它们正常。先让私人仓库的读写与恢复完整走通,再加这些功能。
备份整份应用状态,别只 clone 仓库
clone 可以保存该仓库的 Git 数据,但不等于服务备份。本文的备份命令只适用于数据库、仓库、附件和应用配置都在 ./data 下的 SQLite 实例。若另用了 PostgreSQL、MySQL、外部对象存储或其他挂载,要为那些数据安排一致的备份窗口,不能只打包这个目录。
先确认普通账号的新提交已经可以从空目录 clone,议题与附件也能正常读取,再停服务。应用可能仍有待处理任务:本次演练的完整冷副本保留了 data 内的持久任务队列,恢复启动后,仓库的“是否为空”和更新时间发生了变化;其余仓库字段、议题和附件元数据一致。不能要求运行后的全部 API 字段永远与停机前某一瞬间逐字相同,也不能把保存仓库 Git 对象当成任务状态已经落盘的证明。
在原实例上创建私有备份目录并停止服务。命令失败时先处理错误,避免误把空文件当成备份:
set -eu
umask 077
cd /opt/forgejo
backup_dir="$HOME/forgejo-backups/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$backup_dir"
chmod 700 "$backup_dir"
docker compose config --quiet
docker compose stop forgejo
cp compose.yaml "$backup_dir/compose.yaml"
docker compose config > "$backup_dir/compose.resolved.yaml"
docker image inspect codeberg.org/forgejo/forgejo:16.0.6@sha256:e2c34a699543ca8a00217ee9ab2bea5feedc98bd6d0ea95ec287d3b7ae1355c6 > "$backup_dir/image.inspect.json"
if ! sudo tar --numeric-owner -cpf - -C /opt/forgejo data > "$backup_dir/data.tar"; then
printf '%s\n' '备份失败:保持实例停止,检查错误后再决定是否启动。' >&2
exit 1
fi
test -s "$backup_dir/data.tar"
(cd "$backup_dir" && sha256sum data.tar compose.yaml compose.resolved.yaml image.inspect.json > SHA256SUMS)
if test -f .env; then
cp .env "$backup_dir/.env"
(cd "$backup_dir" && sha256sum .env >> SHA256SUMS)
fi
chmod 600 "$backup_dir"/*
docker compose up -d forgejo
备份命令记录的是镜像信息,不会导出镜像本身;恢复机还要能取得相同摘要的镜像。compose.resolved.yaml、.env、配置和数据库都可能含秘密,应和 data.tar 一起保存到受保护的异地位置。
停止后备份整个 data,不要挑出一个 SQLite 主文件就结束。若目录里仍有 -wal、-shm 或持久任务队列,也要原样保留。本次演练逐个核对了冷副本与恢复启动前的全部 79 个文件及目录,包括内容哈希、数字 UID/GID 和权限;附件还与实际上传的合成文件核对,未只比较文件名或 API 元数据。
恢复前先把这一组文件安全复制到演练机,并在新终端明确填写路径。下面不会覆盖已有目录:
set -eu
umask 077
backup_dir='/实际受保护路径/20261015T000000Z'
backup_file="$backup_dir/data.tar"
test -r "$backup_file" && test -s "$backup_file"
(cd "$backup_dir" && sha256sum -c SHA256SUMS)
restore_dir="$HOME/forgejo-restore-check"
test ! -e "$restore_dir"
mkdir -m 700 "$restore_dir"
if ! sudo tar --numeric-owner -xpf "$backup_file" -C "$restore_dir"; then
printf '%s\n' '恢复失败:不要启动应用,先检查归档和目标目录。' >&2
exit 1
fi
sudo test -f "$restore_dir/data/gitea/conf/app.ini"
sudo stat -c '%n %u:%g %a' "$restore_dir/data"
这里的配置路径对应前面官方镜像正常安装后的默认位置。自定义过路径的实例要按实际配置核对;不要另生成一份新配置盖住旧密钥。普通镜像用的 UID/GID 是 1000/1000,恢复时必须保留整棵目录的数字所有者;只改子文件而把挂载根留给其他身份,仍可能导致启动失败。
演练副本先不接域名、不发布端口,也不给外网。另建 compose.restore.yaml,不要直接启动备份里的原 Compose:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:16.0.6@sha256:e2c34a699543ca8a00217ee9ab2bea5feedc98bd6d0ea95ec287d3b7ae1355c6
restart: "no"
network_mode: none
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__server__ROOT_URL: http://127.0.0.1:3000/
FORGEJO__server__DISABLE_SSH: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__mailer__ENABLED: "false"
FORGEJO__actions__ENABLED: "false"
FORGEJO__cron__ENABLED: "false"
volumes:
- ./data:/data
将这个文件放在 $restore_dir 中,再启动这个独立项目:
cd "$restore_dir"
docker compose -p forgejo-restore-check -f compose.restore.yaml config --quiet
docker compose -p forgejo-restore-check -f compose.restore.yaml up -d
docker compose -p forgejo-restore-check -f compose.restore.yaml logs --tail=100 forgejo
network_mode: none 会阻断副本向外发起邮件、webhook、镜像同步和构建请求;原实例继续使用自己的配置。这个副本也没有浏览器入口,可以先在容器内检查本地 API 和 Git。普通账号必须先登录成功,再判断它有无私库权限,不能把错密码或匿名返回的 404 当成权限隔离通过。
本次用两个普通账号核验:拥有者能读取自己的私库,另一账号登录成功但访问该库得到 404,匿名同样不可读,错密码返回 401。恢复后核对了原提交、议题及附件,再完成一次新提交 push,并从另一个空目录 clone 验证。实测在 Docker Desktop 的 ARM64 容器和专用命名卷内完成,直接启动镜像内应用,通过正常 HTTP API 与 Git 客户端验证。上面的 Linux 绑定目录命令、官方入口初始化、公网 HTTPS 和可视浏览器操作未完整复跑,LFS、邮件、CI、跨主要版本升级也不在这次演练范围内。
只有确认数据、权限和新写入都正确,再安排正式域名切换。演练副本若要开放入口,先确认所有真实外送任务仍停用,并单独验证网络和认证。已有生产实例升级后数据库可能改变,回退要恢复旧版本对应的数据,不能只换回旧镜像。
遇到启动或附件写入失败,先看日志、挂载根与子目录的数字身份、权限和磁盘余量。删掉 /data 会同时删掉账号与仓库,不是修复权限的正常手段。
如果还要从这台 VPS 访问 GitHub 仓库,使用的是另一套账号与仓库权限。遇到 SSH 测试成功而 push 失败时,按GitHub 密钥与仓库权限排查检查;本文的 Forgejo HTTPS 部署不因这一步自动开启 Git SSH。




