这是「Docker 好项目」系列的第 10 期。每期挑一个真正能在自己服务器上跑起来、并且能解决具体问题的项目,给出能直接复制的配置。

代码托管这件事,大部分人被两个极端夹着:想放私有代码,GitHub 免费仓库有限制,而且「你的源代码在别人服务器上」这件事本身就让人不踏实;自己买的服务器明明闲着,却只跑了个监控面板。

搭一套 GitLab 是最出名的「自己动手劝退」项目:官方要求 4G 内存起步,实际跑起来 2 核 4G 的小鸡会被 Ruby + Sidekiq 吃得只剩喘气,1Panel 里装完等它就绪要几分钟。

Gitea 是另一条路:Go 写的单二进制,官方 Docker 镜像空载 200–300MB,2G 内存的服务器就能舒舒服服跑。它是 GitLab/Gogs 那场分叉里活下来并且长得最壮的一支——58k+ stars,MIT 协议,写这篇时最新版 1.27.3(2026-08-29 发布),仓库昨天还有提交,1.27.x 修了二十多个 CVE。管理后台、组织权限、Issue、PR、Wiki、内置 CI(Actions)、软件包仓库、内置 SSH 服务全都有,中文界面开箱即用。

Gitea 效果图

它到底能干什么

  • 私有 Git 托管:不限仓库数、不限协作者、不限私有仓库。这是它和 GitHub 免费版最本质的区别
  • 代码审查:PR、行内评论、必须审查通过才能合并、分支保护规则,团队协作需要的东西都有
  • Issue / 看板 / Wiki:每个仓库自带,也能在组织层开项目看板
  • 内置 CI(Gitea Actions):GitHub Actions 语法兼容,.gitea/workflows/*.yml。需要另跑一个 act_runner 容器才能执行,1.27 配 Runner 2.0
  • 软件包仓库:Maven、npm、Docker、PyPI、Go、Alpine 等十几种包格式都能托管,相当于自带一个私有制品库
  • 仓库镜像:定时从 GitHub/GitLab 双向同步。常用玩法是「GitHub 为主、Gitea 做国内只读镜像」,push 不卡了
  • Webhook:原生支持企业微信、钉钉、飞书、Telegram、Discord、Slack、Matrix、MS Teams、Packagist,事件粒度到 push/issue/PR/release
  • 内置 SSH 服务:容器里自带 sshd,不用依赖宿主机 SSH,git clone git@域名:xxx.git 直接可用

Gitea 界面概览

它不做的是:没有 GitLab 那种大型企业级的功能深度(安全扫描、合规审计、多实例集群);单机部署没有官方高可用方案;Actions 生态还比不了 GitHub 那个量级的第三方 Action 市场。它就是一个「把自己服务器的 Git 服务做到够用且省心」的工具。

部署前准备

  • 内存:空载 200–300MB。个人用 SQLite 就够,2G 内存的小鸡绰绰有余;上 PostgreSQL 再加 100MB 左右
  • 磁盘:Git 仓库按需增长,重点是 LFS 和软件包仓库会悄悄吃盘,给个 10G 起步比较稳
  • 端口:Web 3000,容器内 SSH 是 22(宿主机自己的 sshd 占着 22,所以要映射成 222)
  • 数据目录:容器内 /data,全部家当都在里面。配置文件在 /data/gitea/conf/app.ini
  • 镜像:官方主推 docker.gitea.com/gitea(Gitea 自己的 registry,国内直连速度不错);ghcr.io/go-gitea/gitea 和 Docker Hub gitea/gitea 是同一份镜像——我逐个比过 manifest 摘要,三个源的 1.27 和 1.27.3 完全是同一个 digest,拉哪个都一样

docker run 一行版

先跑起来感受一下:

docker run -d \
  --name gitea \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 222:22 \
  -v /opt/gitea/data:/data \
  docker.gitea.com/gitea:1.27

浏览器打开 http://服务器IP:3000,第一次会进入安装向导——这个向导只有一次机会,见「首次访问」。

docker-compose 推荐版

正式用建议这样部署:

mkdir -p /opt/gitea/data && cd /opt/gitea

新建 docker-compose.yml:

services:
  gitea:
    image: docker.gitea.com/gitea:1.27
    container_name: gitea
    restart: unless-stopped
    user: "1000:1000"
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      TZ: Asia/Shanghai
      # ---- 对外地址,改成你自己的域名 ----
      GITEA__server__DOMAIN: git.你的域名
      GITEA__server__ROOT_URL: https://git.你的域名
      GITEA__server__SSH_DOMAIN: git.你的域名
      GITEA__server__SSH_PORT: "222"
      GITEA__server__SSH_LISTEN_PORT: "22"
      # ---- 安全:跳过安装向导并锁定 ----
      GITEA__security__INSTALL_LOCK: "true"
      GITEA__service__DISABLE_REGISTRATION: "true"
      # ---- 邮件通知(可选,看「通知」一节)----
      # GITEA__mailer__ENABLED: "true"
      # GITEA__mailer__PROTOCOL: smtps
      # GITEA__mailer__SMTP_ADDR: smtp.qq.com
      # GITEA__mailer__SMTP_PORT: "465"
      # GITEA__mailer__FROM: 你@qq.com
      # GITEA__mailer__USER: 你@qq.com
      # GITEA__mailer__PASSWD: 授权码
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "222:22"

启动:

docker compose up -d
docker compose logs -f gitea

看到 Starting new Web server: protocol: http on 0.0.0.0:3000 就成了。

每个配置项为什么这么写

配置 说明
docker.gitea.com/gitea:1.27 :1.27 而不是 :1.27.3。我比对过 registry 的 manifest 摘要:1、1.27、1.27.3、latest 四个 tag 指向同一个 digest——Gitea 的次版本 tag 是真滚动的,补丁发布 1.27 会自动跟上。想绝对锁死就用 1.27.3,latest 千万别用,升级可能直接跳大版本
user: "1000:1000" 容器里跑服务的用户叫 git,UID/GID 默认就是 1000。显式写出来,配合下面两条,保证数据目录属主和进程用户一致,避免「仓库文件 root 写的、Gitea 读不了」这类权限错乱
USER_UID / USER_GID 这个项目是真支持 UID/GID 修改的(entrypoint 脚本里实打实地 sed 了 /etc/passwd),不像 changedetection.io 和 Navidrome 那两期是完全无效的假参数。如果你宿主机的登录用户不是 1000,把它改成宿主机用户的 UID,或者干脆 chown -R 1000:1000 ./data
./data:/data Git 仓库(/data/git/repositories)、数据库(/data/gitea/gitea.db)、配置(/data/gitea/conf/app.ini)、LFS、附件、日志全在这。备份就备份它
/etc/timezone、/etc/localtime 只读挂载 让容器时区跟宿主机走,影响 Actions 的 cron 计划和日志时间戳
GITEA__server__DOMAIN 网页和 clone URL 里显示的主机名。不设就是 localhost,clone 出来的地址会变成 git@localhost:xxx.git,发给同事就闹笑话了
GITEA__server__ROOT_URL 页面跳转、邮件链接、OAuth 回调都基于它。反代到 HTTPS 就必须写 https:// 开头,写成 http:// 会出现「登录后跳回 http」的混合内容问题
GITEA__server__SSH_PORT 写对外暴露的 222。它决定 clone URL 里显示的端口(ssh://git@域名:222/...)。不是容器内监听端口
GITEA__server__SSH_LISTEN_PORT 写容器内监听的 22。sshd 在容器里只认这个。SSH_PORT 管显示、SSH_LISTEN_PORT 管监听,两个名字长得像,弄反了就是「clone 地址带 222 但 222 根本没监听」的经典故障
GITEA__security__INSTALL_LOCK: "true" 跳过安装向导。这个向导是第一次访问才能进的,一旦错过就只能改文件。设了它之后管理员账号得用命令行创建,见下节。注意源码里的逻辑:如果只设了 SECRET_KEY 而 INSTALL_LOCK 留空,会被自动当成 true
GITEA__service__DISABLE_REGISTRATION: "true" 关掉开放注册。公网上不关这个,第二天就会多出一堆注册账号挖矿。要加人走管理员后台或邀请链接
GITEA__mailer__* 邮件发信配置。注意 PASSWD 填的是邮箱服务商的授权码(QQ 邮箱在设置-账户里生成),不是登录密码。不配也能用,只是收不到找回密码和通知邮件
"127.0.0.1:3000:3000" Web 端口只绑本机,公网访问交给反向代理。222:22 不绑 127.0.0.1 是因为 git over SSH 需要客户端能直连,得从公网进

关于 GITEA__ 前缀:这是 Gitea 官方的环境变量覆盖机制,格式是 GITEA__<节名>__<键名>,启动时由 environment-to-ini 写进 app.ini。它在每次容器启动时都会执行一遍——也就是说环境变量的优先级永远高于你手动改的 app.ini。改完 compose 里的环境变量记得 docker compose up -d 重建才生效。

1Panel 怎么搞

Gitea 在 1Panel 应用商店里,应用商店 → 搜 Gitea → 安装。面板会问几个参数:

  • 数据库:个人用选「无 / SQLite」就够,省一个容器;多人团队或仓库量大再选 PostgreSQL
  • HTTP 端口:默认 3000
  • SSH 端口:默认 222,保持就行
  • 端口外部访问:Web 端口建议关(走反代),SSH 端口要开(git over SSH 得直连)

商店装完记得进容器详情核对一下镜像 tag,商店更新偶尔会落后官方几个补丁版本。

不走商店、想用上面那份 compose 的话:容器 → 编排 → 创建编排,把 yml 贴进去,目录选 /opt/gitea,启动即可。

华为云主机上的实操提示:

  • 安全组只放行 222,不要放行 3000。Web 流量从 443 进来转给 127.0.0.1:3000,容器间回环不走公网网卡;222 必须放行,否则只能走 HTTPS push
  • 确认宿主机 22 没被占用。1Panel 装完宿主机 sshd 肯定占着 22,这也是必须映射成 222 的原因——别动宿主机 sshd,直接改 Gitea 的映射

首次访问与最小可用配置

浏览器打开 http://服务器IP:3000。

两种路径,二选一,选完就定死:

  1. 走了安装向导(没设 INSTALL_LOCK):会看到数据库、站点名称、管理员账号一整页表单。注意「服务器域名」和「基础 URL」两栏要填真实域名,别留默认的 localhost——这个向导只出现这一次,提交后想改就得登进容器改 /data/gitea/conf/app.ini
  2. 用上面的 compose(设了 INSTALL_LOCK=true):打开直接是登录页,没有向导。管理员账号用命令行创建:
docker exec -u git gitea gitea admin user create \
  --username lee --password '换成你的强密码' \
  --email you@example.com --must-change-password=false --admin

最小可用配置就三步:

  1. 右上角 + → 新建仓库,建一个私有的测试仓
  2. 本机配置一下 SSH:把你的公钥加到 Gitea 的「设置 → SSH/GPG 密钥」,然后 ssh -T git@你的域名 -p 222,返回 Hi there, xxx! 就通了
  3. clone 下来试一次 push,确认 SSH 端口对

到这步 Gitea 就能干活了。后续加协作者:组织 → 成员 → 邀请,或者仓库 → 协作者,比开放注册安全得多。

Gitea 仓库视图

通知配置

Gitea 的通知分三层,按需启用:

① 站内通知:默认就有,右上角铃铛里看。不依赖任何外部服务。

② 邮件:compose 里的 GITEA__mailer__* 那段,取消注释填上授权码,docker compose up -d 重建。用户在个人设置里勾选「邮件通知」即可收到 Issue、PR、Release 提醒。

③ Webhook:仓库或组织 → 设置 → Webhook → 新建。企业微信、钉钉、飞书都是原生支持的类型,不用像 changedetection.io 那样拼 JSON,选好类型填机器人地址就行:

类型选「企业微信」→ 填 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
类型选「钉钉」   → 填 https://oapi.dingtalk.com/robot/send?access_token=xxx
类型选「飞书」   → 填 https://open.feishu.cn/open-apis/bot/v2/hook/xxx

触发事件按需勾:push、issue 评论、PR 合并、新 release。别全勾,一天 push 十几次会刷屏。

Webhook 建完在列表页有「推送测试」按钮,点一下确认能收到,别等真出了事才发现地址填错。

反向代理与 HTTPS

用第 2 期的 Nginx Proxy Manager:新建 Proxy Host,域名 git.你的域名,Forward 到 127.0.0.1:3000,SSL 页签一键签证书。

两个必须确认的点:

  • ROOT_URL 必须是 https。这是最容易踩的一个:向导或 compose 里写的是 http,反代上了证书,页面能开但登录、clone、Webhook 全部出问题。上面 compose 里已经写对了
  • 反代要转发 X-Forwarded-Proto。Gitea 源码里靠这个头判断是否 HTTPS,NPM 默认带这个头,1Panel 的反代也默认带,基本不用动。自写 nginx 配置的话记得加 proxy_set_header X-Forwarded-Proto $scheme;

用 1Panel:网站 → 反向代理 → 新建,代理地址 http://127.0.0.1:3000,然后在网站设置里签 Let's Encrypt。

SSH 不要走反代。git over SSH 是纯 TCP 长连接,nginx 处理不了。客户端要走 SSH 就直连 ssh://git@你的域名:222/...,安全组放行 222 即可。

踩坑清单

  • rootful 和 rootless 镜像不兼容:Gitea 提供两种镜像,rootful 用 /data + 容器内 22 端口,rootless 用 /var/lib/gitea + /etc/gitea + 2222 端口,目录结构完全不同,不能靠改 compose 里的 image 一行互相切换。选了一种就别换。本文全程用 rootful(功能全、兼容 1Panel 商店)
  • 安装向导只出现一次:首次访问没填对域名/Base URL,之后就一直错。补救:编辑 /data/gitea/conf/app.ini 的 [server] 段,重启容器。或者干脆像推荐配置那样用 INSTALL_LOCK=true 跳过向导,全部走环境变量,可重复、可版本化
  • 改了 app.ini 但没生效:环境变量优先级更高,每次启动都会覆盖一次。改文件没用就说明那个键在 compose 里被 GITEA__ 环境变量管着——去改 compose
  • SSH clone 报 Permission denied (publickey):九成是客户端没加 -p 222,或者 Gitea 上没传公钥。先用 ssh -T git@你的域名 -p 222 验证连通,再排查 key
  • SSH_PORT 和 SSH_LISTEN_PORT 搞反:前者是对外显示端口(写 222),后者是容器内监听端口(写 22)。写反了 clone URL 里会带一个根本没监听的端口
  • 镜像 tag 别用 latest:Gitea 从 1.x 升到 2.x 不会有平滑路径,latest 会在某个时刻悄悄把你带到破坏性升级面前。用 :1.27 或钉死 :1.27.3
  • 升级前必须备份:跨小版本(如 1.26 → 1.27)会跑数据库迁移脚本,迁移只进不退,降级只能靠备份回滚。流程永远是:停容器 → 备份 ./data → 改 tag → docker compose pull && up -d → 看日志确认迁移完成
  • 软件包仓库和 LFS 吃盘:不开 LFS 就明确 LFS_START_SERVER=false;开了的话,/data/git/lfs 会随二进制大文件无限增长,监控一下磁盘
  • 别忘了关注册:公网开放注册 + 无邮箱验证 = 第二天一堆广告账号。要么 compose 里 DISABLE_REGISTRATION=true,要么管理员后台里强制邮箱验证 + 新用户审批
  • 内存不够时先查 Actions:act_runner 默认并发数不低,跑 build 任务时峰值轻松破 G。2G 小鸡把 runner 的 labels 收窄、并发调成 1,或者干脆把 runner 单独部署到另一台机器

备份

最省事的是官方自带的 gitea dump,一个命令打包全部:

docker exec -u git gitea gitea dump -c /data/gitea/conf/app.ini \
  -f /data/gitea-dump.zip --skip-index

-f 指定输出文件(落在 /data 卷里就能在宿主机看到),--skip-index 跳过可重建的搜索索引(bleve),减小体积。还可以加 --skip-lfs-data、--skip-attachment-data、--skip-package-data 按需排除大块头。

恢复到新机器:解压 zip,把 data/ 和 repos/ 按原路径放回 /data,把 app.ini 放到 /data/gitea/conf/,起容器即可。

如果你更想直接拷目录,也能这么干:

cd /opt/gitea
docker compose stop gitea
tar czf gitea-backup-$(date +%F).tar.gz data/
docker compose start gitea

注意先停容器——Gitea 运行中写 SQLite,热拷贝可能拷到半个事务。

在 1Panel 里可以做成计划任务:每天凌晨跑一次上面的 dump 或 tar,输出到备份目录,配合面板的备份策略推到对象存储。这是本系列第 4 期 Immich 的同款做法。

小结

Gitea 是这系列里功能密度最高的一期:Git 托管、代码审查、Issue、CI、制品库、Webhook、内置 SSH,全塞在一个 300MB 的容器里。它不是 GitLab 的完整替代,但它把「个人或小团队在服务器上管自己的代码」这件事做到了不留遗憾——部署快、资源省、中文界面、文档齐全,是 Docker 自托管绕不开的一站。

适合:私有代码不想放别人服务器、团队 3–20 人需要内部代码审查、做 GitHub 国内镜像加速、想给自己的服务器项目一个「正经的家」。

不适合:需要 GitLab 级别的合规审计、安全扫描、多实例高可用(那是 GitLab 的地盘);只是想「备份几个 dotfiles」的小需求,直接用 GitHub 私有仓库更省事。

下期继续聊另一个 Docker 项目。


项目地址:https://github.com/go-gitea/gitea 官方文档:https://docs.gitea.com/