
一台 2 核 4G 的云主机,装完系统剩下 38G 空间,跑一个自建 Git 服务绰绰有余。真正占时间的不是安装,是安装完之后那四个会静默失败的地方。
这篇写的是一套能直接复制运行的配置:Gitea 1.24 系列、PostgreSQL 16、Nginx 反代、SSH 走 2222 端口。所有版本号都写死,避免某天自动升级把数据目录顶掉。
1. 目录怎么摆
先建目录,再动手写配置。目录结构决定了以后备份和迁移的难度,一开始摆错,后面每次操作都要多记一条路径。把「程序」和「数据」分开,是这套部署里唯一不能妥协的原则。
/opt/gitea
├── docker-compose.yml
├── db/ # PostgreSQL 数据,容器写入
└── data/ # Gitea 全部家当
├── gitea/conf/app.ini
├── git/repositories/ # 裸仓库
├── lfs/ # 大文件
├── avatars/
└── backup/ # gitea dump 的输出
/etc/nginx/conf.d/gitea.conf
data 这一个目录就是全部资产。仓库、附件、头像、配置、密钥都在里面,迁机器时把它和数据库一起搬走就能复原。除它之外的东西都可以重建,包括容器。
2. 第一份配置:docker-compose.yml
数据库单独一个容器,不要用 Gitea 内置的 SQLite。这不是性能问题,是备份问题:SQLite 在写入过程中做文件级拷贝,拿到的是一个损坏的库。单人多仓库用 SQLite 也能跑,但只要涉及定时备份,Postgres 的 pg_dump 是唯一省心的选项。
services:
db:
image: postgres:16
restart: always
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: change-me-db
POSTGRES_DB: gitea
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"]
interval: 10s
timeout: 5s
retries: 5
server:
image: gitea/gitea:1.24.3
restart: always
depends_on:
db:
condition: service_healthy
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: change-me-db
GITEA__server__SSH_PORT: 2222
GITEA__server__SSH_LISTEN_PORT: 22
GITEA__server__ROOT_URL: https://git.example.com/
GITEA__service__DISABLE_REGISTRATION: "true"
ports:
- "127.0.0.1:3000:3000"
- "2222:22"
volumes:
- ./data:/data
- /etc/localtime:/etc/localtime:ro
三个细节值得单独说明。3000 端口绑在 127.0.0.1 上,不对外,外网只走 Nginx 的 443;USER_UID 和 GITEA__server__SSH_PORT 必须同时出现,前者决定写文件的身份,后者决定页面上给出的克隆地址;DISABLE_REGISTRATION 要在第一次启动前就设好,否则公开的注册页会在你发现之前积下几十个垃圾账号。
启动之前先把数据目录的属主改掉。这一步现在做,能省掉后面一次莫名其妙的 push 失败。
mkdir -p /opt/gitea/db /opt/gitea/data
chown -R 1000:1000 /opt/gitea/data
cd /opt/gitea && docker compose up -d
docker compose logs -f server | head -40
3. 第二份配置:app.ini 里真正要改的五项
首次启动后,Gitea 会在 data/gitea/conf/app.ini 生成一份完整配置,你不需要从零写,只需要改五行。改环境变量也行,但环境变量和文件同时存在时,以文件为准,这是最容易互相打脸的地方。
[server]
DOMAIN = git.example.com
ROOT_URL = https://git.example.com/
SSH_DOMAIN = git.example.com
SSH_PORT = 2222
SSH_LISTEN_PORT = 22
HTTP_PORT = 3000
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
[repository]
DEFAULT_BRANCH = main
ROOT = /data/git/repositories
[lfs]
PATH = /data/lfs
[security]
INSTALL_LOCK = true
SSH_PORT 和 SSH_LISTEN_PORT 的区别必须记清楚。前者是对外公布的端口,会写进克隆地址;后者是容器内 sshd 实际监听的端口。写反了的后果是:网页上给的地址是 ssh://git@host:22/…,而宿主机的 22 端口被系统 sshd 占着,clone 会一直在那转圈直到超时。
INSTALL_LOCK = true 也建议手动加上。它锁掉网页安装向导,避免某次重启后配置被重置成初始状态。
改完重启,然后建第一个管理员。不要靠注册页建,那是最容易留下隐患的路径。
docker compose restart server
docker compose exec -u git server \
gitea admin user create \
--username guo --password 'Your-Strong-Pass' \
--email guo@example.com --admin --must-change-password=false
4. 第三份配置:反代与证书
反代这一段只有三个必填项,漏掉任何一个,症状都会在几天后才出现。Host 决定生成链接的域名,X-Forwarded-Proto 决定链接是 http 还是 https,client_max_body_size 决定你能不能推大文件。
server {
listen 443 ssl http2;
server_name git.example.com;
ssl_certificate /etc/letsencrypt/live/git.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem;
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
client_max_body_size 默认是 1m。不改这一行,push 一个带 20MB 素材的提交会直接被 Nginx 拦下,返回一个和 Git 毫无关系的 413。
如果反代和 Gitea 不在同一台机器,还要在 app.ini 里加 REVERSE_PROXY_TRUSTED_PROXIES,否则 Gitea 会把所有请求的来源 IP 记成反代的地址,登录失败次数限制会误伤所有人。一个所有用户共享的 IP,等于一个所有用户共享的风控额度。
这套配置在 2 核 4G 的机器上,空载内存大约占 600MB 到 800MB,其中 Postgres 和 Gitea 各一半。五十个仓库、日常三五个人的提交量,CPU 基本闲着。真正会把机器拖死的是两件事:一次性导入几百个仓库的历史,和有人在网页上打开一个两万行的 diff。
如果仓库数量会超过两三百个,内存建议给到 8G 以上,并把 Postgres 的 shared_buffers 显式设成 1GB 左右。默认值是 128MB,在大仓库的 gc 阶段会成为瓶颈。
5. 把已有的仓库搬过来
搬仓库有两种做法,选哪种取决于你要不要保留 issue、wiki 和 release。只要代码的用镜像推送,全套元数据都要的用网页端的迁移功能。
只要代码,用 --mirror 推一遍最干净。它会把所有分支、所有标签、所有远程引用原样搬过去,不经过工作区。速度取决于带宽,不取决于仓库里有多少文件,这是它比网页迁移可靠的地方。
git clone --mirror https://github.com/old/project.git
cd project.git
git push --mirror ssh://git@git.example.com:2222/guo/project.git
cd .. && rm -rf project.git
要保留 issue 和 wiki,用 Gitea 的「迁移仓库」页面,填旧仓库地址和访问令牌。迁移是异步任务,页面关掉它照样在跑,但失败时不会弹窗,只会在「管理后台 – 监控 – 队列」里留一条错误。
迁移完成后有一件事必须手工确认:默认分支。旧仓库如果默认分支是 master,而你在 app.ini 里设了 DEFAULT_BRANCH = main,迁移过来的仓库会显示一个空首页。它没坏,只是把不存在的 main 当成了首页。
网页迁移还有一个容易忽略的限制:单仓库大小和超时。超过一定体积的仓库会因为反向代理的 proxy_read_timeout 被切断,日志里只有一行 504。大仓库老老实实用镜像推送,别指望网页端。
6. 验收:四条命令
装完不要只看网页能不能打开。下面四条按顺序跑,每条都对应一个独立的链路,任何一条失败都指向不同的层。能打开首页只证明 Nginx 和 HTTP 通了,其余三条才证明这套东西能用。
curl -sI https://git.example.com/api/healthz应返回 200,证明反代与后端都在。ssh -T -p 2222 git@git.example.com应返回一句欢迎语加你的用户名,证明 SSH 链路和公钥都认了。- 新建一个空仓库,
git clone ssh://git@git.example.com:2222/guo/test.git,能克隆说明仓库路径与权限正常。 - 提交一个 2MB 的二进制文件并 push,再
git lfs ls-files,确认 LFS 真的接管了它。
第二条命令最容易失败,而且报错信息通常只有一句 Permission denied (publickey)。它同时可能是密钥没上传、端口错、容器内 sshd 没起、或者宿主的 2222 被别的服务占了,四种原因一个提示。
7. 四个必然踩到的坑
下面四条不是假设,是按这套配置走一遍几乎一定会遇到的。提前知道症状,比事后看日志快得多。
坑一:push 时报 insufficient permission for adding an object。原因是 data/git/repositories 的属主不是 1000。容器内 git 用户的 UID 是 1000,宿主机上如果目录是 root 建的,写权限就没有。修法是 chown -R 1000:1000 /opt/gitea/data,不要用 chmod 777 绕过去,那只是把问题藏起来。
坑二:SSH 端口改对了,克隆地址还是 22。原因写在上一节:SSH_PORT 没改,或者改了没重启。Gitea 生成克隆地址时只读 SSH_PORT,它不知道你的 docker 端口映射表。
坑三:网页登录后跳回 http,或者邮件里的链接点不开。两个原因,ROOT_URL 写了 localhost:3000,或者反代没传 X-Forwarded-Proto。前者影响所有绝对链接,后者只影响需要拼协议的页面,所以症状时有时无,最难查。
坑四:直接改 compose 里的 postgres 标签做升级,容器起不来。Postgres 的大版本升级要重写数据目录格式,16 的数据文件用 17 的镜像打不开。正确顺序是 pg_dump 导出、换镜像、导回,中间不能省。
8. 备份与升级,这两件事不能等到出事
备份不要用 rsync 或 tar 直接打包 data 和 db。仓库在写入时打包,拿到的是一个能解开但没法用的快照,而且这种损坏不会在解压时报错。它只会在你真正需要恢复的那天报错。
docker compose exec -T -u git server gitea dump -c /data/gitea/conf/app.ini -f /data/backup/gitea-daily.zip
把它挂进 crontab,每天凌晨跑一次,输出文件名带上当天日期;保留 14 天,再按修改时间定期清理旧包。排程本身不是难点,难点是「跑成功但包是空的」这种情况——它不会报错,只会让恢复那天发现少了一个月的提交。
gitea dump 会把仓库、数据库、配置、附件打成一个包,但它不含 LFS 对象和 data/backup 自身。如果你启用了 LFS,要把 data/lfs 单独纳入备份,否则恢复后仓库能打开,大文件全是断链。
恢复的顺序也要记一下:先起一个空数据库,导入 dump 里的 SQL,再把 repositories 和 lfs 放回原位,最后启动。顺序错了不会报错,只会得到一个空站点。
写在最后
自建 Git 服务的门槛从来不在安装。docker compose up 之后十分钟内你就能看到登录页,这件事太顺,顺到让人以为已经完成。真正的成本在之后的每一次端口变更、每一次证书续期、每一次大版本升级里。
托管服务卖的不是服务器,是这些琐事有人替你处理。自建换来的不是省钱,是知道数据在哪、以及出事时能自己动手。这两件事对不同的人价值完全不同,选之前最好先想清楚自己要的是哪一个。
如果只是想给自己几个私人仓库找个落脚点,一台 2 核 4G 的机器加上这四份配置就够了。但它从此是你的系统,坏了没人替你修。
你自建的哪一项服务,最后因为升级或迁移太麻烦而放弃了?

评论 (0)