用 Gitea 28 搭建私有 Git 仓库:VPS 部署、只读拉取与审计记录

用Gitea28在VPS上托管私有Git仓库,配置Docker Compose与Caddy HTTPS,为部署机器提供仓库范围的只读令牌,并开启审计、明确记录边界与备份范围。

·12 minDockerVPSGiteaGit
把应用部署到土耳其|BRNCHOST · 土耳其 VDS
云服务器,积分可续期|雨云 · 国内外节点 · 积分兑换权益
低价年付,大流量 VPS|RackNerd · SSD 存储 · 1Gbps 端口
香港轻量,搭个小站|晚安云 · 香港云服务器
香港 VPS,大带宽可选|野草云 · BGP 直连
大陆访问,精品线路|搬瓦工 · CN2 GIA / CTGNet 套餐
资料归档,交给 AI 整理|WorkBuddy · 本地文件处理
建站起步,先看应用镜像|腾讯云 · 轻量应用服务器
CN2 GIA,中国方向优化|DMIT · Premium 网络
双 ISP 原生住宅 IP|丽萨主机 · 美国 9929 精品线路
高频 CPU,多地部署|Evoxt · 云服务器 · 每周异地备份
京东云轻量云主机:129元/年,新人专享,限购1台

网站部署机器需要拉取代码,通常不需要拥有开发者账号的全部权限。把个人账号的长期令牌复制到几台服务器上,项目一多,每份凭据给了谁、能访问哪些仓库,就需要单独记录。自建 Gitea 时,除了让 Git 能推拉,也要把这层权限安排好。

Gitea 28 增加了仓库范围的 HTTPS 部署令牌和审计日志,适合用来处理这个问题。本文给一个小型私有代码托管方案:Docker Compose 运行 Gitea,宿主机 Caddy 提供 HTTPS,开发者正常提交,部署机器只读拉取。暂不部署 Actions Runner,也不把构建任务与 Git 服务一起塞进同一台 VPS。

示例采用 Gitea 28.0.0 的普通 Docker 镜像和 SQLite,适合先建立单机服务。已有 Gitea 要走升级流程,不能把下面的目录和初始化配置直接覆盖到现有实例上。团队并发、仓库体积和功能范围增长之后,再按实际需求评估外部数据库、存储与独立构建环境。

28.0.0 的版本号,和这篇教程的范围

官方发布说明解释了版本号变化:此次去掉了历史上的 1. 前缀,28.0.0 对应原来的 1.28.0 命名方式。看到数字变化,不必以为中间突然跨过了二十多个版本。

本文使用固定标签 docker.gitea.com/gitea:28.0.0。这样部署文件、配置讨论和故障记录指向同一个版本;以后更新时仍要检查发布说明,并重新安排备份和维护时间。

这里选择普通镜像。Gitea 的普通与 rootless 镜像有不同的部署约定,官方明确提醒不要通过改一个镜像标签来互相切换。服务器上已经使用 rootless 方案的读者,应继续使用对应文档,不照搬本文挂载和用户配置。

先确定域名、入口与数据目录

准备一个解析到 VPS 的域名,例如 git.example.com,并安装 Docker Engine 与 Compose 插件。示例假定 Caddy 已经运行在宿主机,且没有其他服务占用本机 3000 端口。正式入口使用 HTTPS,容器的 Web 端口只绑定到回环地址。

这套方案通过 HTTPS 推拉 Git,不开放容器 SSH。VPS 的运维 SSH 入口继续使用原来的配置,不要因为部署 Gitea 就修改系统 SSH 端口或账号。以后确实需要 SSH Git,再单独规划端口映射、防火墙和仓库页面显示的 SSH 地址。

在自己有写入权限的位置创建独立部署目录:

mkdir -p ~/services/gitea
cd ~/services/gitea
mkdir -p data
sudo chown -R 1000:1000 data

本文让容器内的 Git 用户使用 UID/GID 1000。这个数字不是通用的权限修复方法:宿主机有自己的目录所有权安排时,应选合适的 UID/GID,并同时调整下面的环境变量。不要为了让服务启动,把数据目录改成所有人可写。

数据目录包含仓库、数据库和配置等持久化内容,需要预留空间,也要进入备份范围。不要把这个目录当成容器缓存随意清理。若开启 LFS、软件包仓库或 Actions,空间占用和备份范围会进一步扩大。

Compose 中把初始化与权限写清楚

创建 compose.yaml。把 git.example.com 改成自己的域名,保留两处地址的一致性:

services:
  gitea:
    image: docker.gitea.com/gitea:28.0.0
    container_name: gitea
    restart: unless-stopped
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      GITEA__database__DB_TYPE: sqlite3
      GITEA__server__ROOT_URL: https://git.example.com/
      GITEA__server__DISABLE_SSH: "true"
      GITEA__service__DISABLE_REGISTRATION: "true"
      GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
      GITEA__audit__RECORD_OUTPUT: database
      GITEA__audit__RETENTION_DAYS: "30"
    volumes:
      - ./data:/data
    ports:
      - "127.0.0.1:3000:3000"

ROOT_URL 用于生成对外地址。Gitea 28 不再读取旧的 [server] DOMAIN,所以不要只从旧教程中抄一个 DOMAIN 就认为地址配置完成。环境变量按 GITEA__配置段__配置项 的形式写入配置,具体规则见官方 Docker 文档。

示例显式关闭自助注册,并要求登录后浏览。仓库本身仍应选为私有;站点登录要求、仓库可见性和成员权限是不同设置,不能只配其中一个。初次安装时需要建立管理员账号,安装完成后其他成员由管理员按实际需要创建或邀请。

执行配置检查并启动:

docker compose config --quiet
docker compose pull
docker compose up -d
docker compose logs --tail=100 gitea

如果日志提示数据目录权限错误,先核对挂载目录和 UID/GID;如果端口冲突,检查宿主机服务。配置检查通过只说明 Compose 文件可以解析,不能证明网页、数据库和 Git 操作已经可用。

初次安装页面不宜长期暴露。可以先通过 SSH 隧道在自己的电脑访问,完成管理员与数据库初始化,再开放正式入口:

ssh -N -L 13000:127.0.0.1:3000 user@your-vps

user@your-vps 是自己的服务器账号和地址。本机浏览器打开 http://127.0.0.1:13000,在安装页面确认 SQLite 和持久化目录,创建管理员。标准站点地址仍填写正式的 https://git.example.com/,不要把临时隧道地址保存为 ROOT_URL。

用 Caddy 提供正式 HTTPS 地址

宿主机 Caddy 的站点配置可以从下面这一段开始:

git.example.com {
    reverse_proxy 127.0.0.1:3000
}

域名解析与 80、443 入口需要满足证书签发和访问要求。若 Caddy 使用已有的集中配置,按原来的方式检查配置并重载,避免覆盖其他站点。Caddy 也在容器里时,127.0.0.1 指向 Caddy 容器本身,应改为共享网络中的 Gitea 服务地址。

打开正式域名后,检查登录跳转、仓库链接和 HTTPS 克隆地址是否都指向这个域名。不要通过忽略证书错误来掩盖 ROOT_URL、证书或代理配置问题。需要子路径部署时还涉及额外路径处理,本文的独立子域名配置不能直接拿来用。

在管理员账号里创建第一个私有仓库,例如 site-code。本机克隆时使用仓库页面提供的 HTTPS 地址:

git clone https://git.example.com/your-owner/site-code.git

替换域名和仓库所有者。开发者使用自己的 Git 认证凭据;开启双因素认证后,按 Gitea 提示使用适合 Git HTTPS 的令牌方式。不要把账号密码或令牌写进克隆 URL,避免它出现在命令历史和仓库的远端配置中。

给部署机器一份只读凭据

仓库范围的 HTTPS 部署令牌可以用于 Git 和 LFS 的 HTTPS 认证,每份令牌只对应一个仓库,可选择只读或读写。部署任务只是拉取代码时,选只读,不给推送权限。它与个人访问令牌的用途和权限范围不同,也不应当作通用后台 API 凭据。

在仓库的设置中找到部署凭据相关入口,创建令牌并给它一个能辨认机器与用途的名称,例如 web01-pull-site-code。记录它给了哪台机器;另一台服务器另建一份,不共享同一份长期凭据。这样机器退役、项目迁移或者怀疑泄漏时,可以单独撤销。

Gitea 官方示例中的仓库 HTTPS 部署令牌设置

图为 Gitea 28 官方发布说明中的界面示例。实际入口以自己的版本和界面语言为准;创建时检查只读选项,并及时保存令牌。API 文档说明,明文令牌只在创建时返回,不要等机器部署到一半才回头找原值。

部署令牌作为 HTTPS Git 认证的密码使用,用户名按 Gitea 的提示填写。无人值守环境应通过自己的凭据管理方式提供令牌,并限制对应文件和运行账号的权限。不要把它提交到 Compose 仓库、部署脚本或公开日志里,也不要把它放进正文展示的示例命令。

准备接入之前,确认这份凭据能拉取指定私有仓库;测试推送权限时只使用可丢弃的测试仓库,不能在生产仓库里随便试写。部署流程也要固定分支、标签或提交,不能每次拉一个变化中的默认分支就认定发布版本相同。

只读拉取与代码可信度还要分开考虑。令牌只读不会审查仓库里的代码;如果部署机器拉取后直接执行脚本,仓库写权限和分支保护仍然会影响这台机器。给开发者和自动化账号最少所需的权限,审核部署分支,也保留部署版本记录。

审计日志要开启,也要知道它没有记录什么

上面的 Compose 已设置 GITEA__audit__RECORD_OUTPUT=database。Gitea 28 的审计记录默认关闭,配置生效并重启后才开始写入;过去没有记录的操作不会因为现在打开了开关而自动补齐。

实例管理员可以在站点管理的监控区域查看 Audit Log;仓库管理员、组织所有者和普通用户有各自范围的查看入口。检查权限与部署凭据变更时,根据操作者、动作和来源过滤,再查看对应事件。需要保存筛选结果时,实例管理员可导出 JSONL。

Gitea 官方示例中的审计日志详情

图为官方审计界面示例。这里查看的是安全相关操作记录,与日常排查错误的应用日志不同。按审计文档列出的范围,仓库可见性、协作者、部署凭据等变更属于可以记录的操作。

需要注意,普通浏览、clone、fetch 和 API GET 等只读访问不在这套审计记录范围内;登录也不是一律被记录。不能据此判断“某台机器从来没有拉过代码”,或把没有事件当成没有访问。需要排查访问与认证问题时,还要结合代理、访问日志和其他实际记录。

示例保留 30 天。项目有更长的追溯需求时可以调整 RETENTION_DAYS,但延长时间也增加数据库占用。设置为 0 表示永久保留,不是关闭审计。不要一边依赖它追溯,一边让日志保留策略在没有记录的情况下改变。

同样,审计写入失败不会让原本的操作失败。要检查日志里的写入错误,不能认为“仓库操作成功”就等于“审计事件一定保存成功”。导出的事件也包含人员、仓库和操作信息,应与其他运维资料一样限制访问。

备份时,不要只保留一份 Git 克隆

本机完整克隆可以保存 Git 历史,但不会自动保存站点账号、权限、问题讨论、服务配置,以及 Git 数据之外的所有附件。开启 LFS 或软件包存储后,更要检查对象放在哪里。恢复整个 Gitea,需要这些内容与数据库配套。

对本文的单容器、SQLite、./data 挂载方案,可以安排停写窗口,停止服务后归档完整持久化目录,同时保存 Compose 和代理配置。下面仅适用于数据确实全部在这个目录、没有其他写入进程的场景:

umask 077
backup_dir="./backups/gitea-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
cp compose.yaml "$backup_dir/compose.yaml"
docker compose stop gitea
sudo tar -czf "$backup_dir/data.tar.gz" -C ./data .
sudo tar -tzf "$backup_dir/data.tar.gz" > /dev/null
docker compose start gitea

打包失败时先确认问题,不继续升级或删除旧文件。归档可读取不等于应用已能恢复;备份再复制到服务器之外,并安排一次独立目录的恢复检查。不要把唯一备份放在与仓库相同的磁盘上。

用外部数据库或对象存储时,这个 tar 命令不覆盖对应数据。官方还提供 gitea dump,但不同部署方式的执行用户、临时目录和数据库导出条件不同,应按备份文档处理,而不是照抄一个命令就认为所有数据都在压缩包里。

以后需要启用 Actions,再增加独立 Runner 并规划它能接触的网络、目录和凭据。Gitea 28 对已完成的 Actions 运行记录默认采用 400 天保留时间,作业、日志和产物会随运行记录删除;长期保存发布证据的团队应事先设置保留策略。只做代码托管与 HTTPS 拉取的阶段,不必为了功能齐全立即开启这些执行能力。