主题
自建 Gitea:半小时能用的最小组合
需求很朴素:一个自己能控制的 Git 服务,有网页界面、能 HTTPS clone、私有仓库、不依赖任何第三方。
Gitea 是这个需求下最省事的选择:单个二进制 / 单容器、SQLite 就能跑、资源占用低、界面完整。这篇是我在 NAS 上把它跑起来并接到公网的实际步骤和踩过的坑。
架构(最小可用)
浏览器 ──https──> Cloudflare 隧道 ──http──> 127.0.0.1:3000 (Gitea Web)
git@ ──ssh───> 宿主 2222 端口 ────────> 容器 22 (Git SSH)
│
/vol1/1000/docker/gitea/data (仓库 + SQLite + 配置)三个关键决定:
- SQLite:家用场景没有并发压力,少一个数据库容器就少一个要备份的东西
- Web 端口 3000:走隧道出去,不需要开防火墙
- SSH 用 2222:宿主的 22 被系统占了,映射到宿主 2222
一、拉镜像(国内网络的必要绕路)
bash
docker pull docker.m.daocloud.io/gitea/gitea:1.27.3
docker tag docker.m.daocloud.io/gitea/gitea:1.27.3 gitea/gitea:1.27.3镜像源的对比见 拉不到镜像怎么办。要点:tag 回标准名,这样 compose 文件里写的是标准镜像名,以后换个源不用改文件。
二、用环境变量跳过安装向导
Gitea 第一次启动会进网页安装向导。别走那个向导 —— 它生成的 app.ini 藏着一堆默认值,容器重建时容易丢。全用环境变量:
yaml
services:
gitea:
image: gitea/gitea:1.27.3
container_name: gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.example.com
- GITEA__server__ROOT_URL=https://git.example.com/
- GITEA__server__HTTP_PORT=3000
- GITEA__server__SSH_PORT=2222 # 显示给用户 clone 用的端口
- GITEA__server__SSH_LISTEN_PORT=22 # 容器内监听端口
- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__security__INSTALL_LOCK=true # 跳过安装向导
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "2222:22"⚠️ INSTALL_LOCK=true 是跳过向导的关键。不设它,第一次访问会被 302 到 /install。
⚠️ Web 端口绑 127.0.0.1,不要 0.0.0.0。外网访问统一走隧道,别多开一个入口。
三、坑一:SSH 端口冲突 → 容器无限重启
我一开始同时设了:
START_SSH_SERVER=true # 让 Gitea 自己起内置 SSH 服务结果容器无限重启。原因:镜像里自带了一个 OpenSSH 监听 22,再加内置 SSH 服务也去抢 22 → 端口被占 → 启动失败 → 重启循环。
去掉 START_SSH_SERVER 就好。默认行为(用镜像里的 OpenSSH + Gitea 注入 authorized_keys)就是你要的。
判断方法:容器一直 restarting 时,先看日志最后几行,通常就是 bind: address already in use 这种。
bash
docker logs --tail 30 gitea四、坑二:接隧道时 service 必须带 scheme
注册隧道域名时:
bash
# ❌ Cloudflare 报 1056(origin 配置无效)
{"domain":"git.example.com","service":"localhost:3000"}
# ✅
{"domain":"git.example.com","service":"http://localhost:3000"}这个坑的细节(包括凭据内存缓存、一个 appName 只能一个域名)在 闸道那篇 里。
五、验证:别只测首页
部署完"首页 200"什么都证明不了。Git 服务最少验证这四条:
bash
# 1) 外网 HTTPS 首页
curl -s -o /dev/null -w '%{http_code}\n' https://git.example.com/
# 2) HTTPS clone(公开仓库,或带 token)
git clone https://git.example.com/<user>/<repo>.git /tmp/t1
# 3) SSH 认证(这条最容易挂)
ssh -T git@git.example.com -p 2222
# 期望输出类似:Hi <user>! You've successfully authenticated...
# 4) SSH clone
GIT_SSH_COMMAND='ssh -p 2222' git clone git@git.example.com:<user>/<repo>.git /tmp/t2第 3 步是最有诊断价值的:它同时验证了端口映射、authorized_keys 注入、容器内 sshd 状态。首页 200 但 ssh -T 失败,说明只有 Web 那半边是通的。
⚠️ 测试完记得清理:测试仓库、临时密钥、/tmp/t1 /tmp/t2。
六、管理员和注册
- 第一个注册的用户就是管理员。所以要么先注册自己,要么直接关掉注册
- 生产环境建议:关闭公开注册(
DISABLE_REGISTRATION=true),需要的人由管理员在后台建
七、备份(Gitea 有官方工具)
仓库数据在 /data 里,但直接拷 /data 不是最好的做法,因为 SQLite 可能正在写。用官方 dump:
bash
docker exec -u git gitea gitea dump -c /data/gitea/conf/app.ini -f /data/backup/gitea-dump.zip出来的 zip 包含仓库、数据库、配置、密钥。异地备份时记得加密 —— 里面有 SSH 私钥和用户密码哈希。
加密 + 云挂载的完整姿势见 云挂载为什么不能直写(注意:不能直接 > 写到云挂载点)。
八、升级注意
Gitea 的版本升级有时候带数据库迁移。所以:
- 升级前先跑一次
gitea dump - 镜像 pin 到具体版本号(别用
:latest),我在 compose 里写死1.27.3就是为此 - 升级后第一次启动会跑迁移,日志里能看到,别急着 Ctrl+C
检查清单
- 镜像走加速源拉,
tag回标准名 - 环境变量配全 +
INSTALL_LOCK=true跳过向导 - Web 端口只绑
127.0.0.1,外网走隧道 - 不要设
START_SSH_SERVER(会撞端口) - 隧道
service带http://前缀 - 四条验证:首页 / HTTPS clone /
ssh -T/ SSH clone - 关掉公开注册
- 用
gitea dump备份,异地包要加密
一句话版本:Gitea 最小可用 = SQLite + 环境变量 +
INSTALL_LOCK+ 隧道;两个必踩的坑是 SSH 端口冲突和隧道service缺 scheme。