Gitea
pg addon install gitea 以受管容器方式运行 Gitea——轻量级自托管 Git 服务——直接跑官方上游镜像(docker.io/gitea/gitea,rootful 构建)。与 Grafana 一样,它是顶层插件:不依附于任何单个 PostgreSQL 实例,也不自动前置它们——但可以把某个 PG 实例用作自己的元数据库(见下文 --dsn)。单个容器同时提供 Web UI、Git HTTP 流量,以及 git-over-ssh(跑在镜像自带的 OpenSSH sshd 上——见下文「Git SSH」小节;无论 --ssh-port 与否这个 sshd 都在跑,该 flag 只决定它是否是活的克隆端点)。
安装全程非交互:INSTALL_LOCK=true 跳过 Gitea 的 Web 初始化向导,admin 账号随后由容器内 gitea admin user create 命令行创建。Web 登录密码在首次安装时自动生成、打印一次、存入 pg.yaml,之后用 pg addon password gitea 取回。Listen 默认 0.0.0.0——Git 服务天然要给别的机器克隆——暴露面由自动生成的强密码与关闭自助注册兜底;只想本机访问就传 --listen 127.0.0.1。
平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在
pgcli-netbridge 上跑在 podman machine 里,发布 Web 端口、以及启用时的 SSH 端口),但尚未在 Mac 上验证过。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--name |
gitea |
实例名(map 键) |
--image |
docker.io/gitea/gitea:28.1.0 |
覆盖镜像(仅限 rootful 构建,见已知限制) |
--port |
自动(基址 3080) | Web 宿主端口;自 gitea_start_port 自动分配 |
--ssh-port |
未指定 | 让那个一直在跑的 sshd 变成活的 git-over-ssh 克隆端点(见下文「Git SSH」小节)。pgcli 给每一个实例都从 gitea_ssh_start_port(基址 2222)分配一个端口用来 park sshd 离 22,SSH 开或关都一样;--ssh-port 0 关掉克隆语义、端口占用不释放 |
--listen |
0.0.0.0 |
绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环。同时也是 Gitea 渲染进 clone URL 的主机名 |
--admin-user |
gitea |
Web 登录账号(故意不用 git——那是容器内的 OS 用户,ssh:// 克隆认证的就是它) |
--password |
自动生成 | Web 登录密码(仅在 admin 账号尚不存在时生效,见下) |
--dsn |
空 | 用 postgres://user:pass@host:port/db 把元数据放到外置 PostgreSQL,替代内嵌 SQLite;目标库必须已存在 |
--allow-registration |
false | 打开自助注册(Web UI 出现注册入口)。默认关闭——见「监听与暴露」一节 |
--data-dir |
自动 | 覆盖数据目录(默认在 base 目录下) |
--force |
false | 即使正在运行也重建——用于应用改动的端口、凭据、dsn、监听地址或注册开关 |
安装
命令会按需拉取镜像、创建并启动容器,等 Gitea 首启迁移跑完,创建 admin 账号,然后打印:
用浏览器打开 URL,以打印的凭据登录,在 Web UI 里建第一个 org/repo。HTTP 克隆用同一个登录:
数据目录与 /data
Gitea 的所有资产都在这一个 bind 挂载目录里:仓库、渲染出的 /data/gitea/conf/app.ini、SQLite 元数据库(/data/gitea/gitea.db——用 --dsn 时不存在)、SSH host key、日志。它跨 pg addon remove 存活(用 --clean-data 才删除),重装同名实例即复活一切。
镜像的 rootful 构建以 git 用户运行——uid/gid 1000,经 USER_UID/USER_GID 钉死,宿主的预 chown 与容器进程从此不可能漂移——而其 entrypoint 只 chown 它自己创建的文件,所以 pgcli 在创建容器前先在宿主上把数据目录 chown 过去:rootless podman 下经 podman unshare chown 1000:1000,rootful 宿主且 pgcli 以 root 运行时直接递归 chown。
pgcli 绝不注入 SECRET_KEY 或 INTERNAL_TOKEN。 Gitea 每次启动都会把所有 GITEA__<section>__<KEY> 环境变量折叠进持久化的 app.ini;若 pgcli 每次安装重新生成并注入加密 secret,那么在保留数据目录的重装上,新的 SECRET_KEY 会覆盖持久 app.ini,而幸存的数据库里 2FA 种子与 OAuth client secret 仍是按旧密钥加密的——一次无报错、无日志的静默锁死。所以这些密钥归 volume 所有:Gitea 首启自生成,pgcli 只拥有 admin 登录密码。
admin 密码只在数据库里不存在该 admin 账号时生效——账号是 gitea admin user create 建出来的,pgcli 从不重置。在保留的数据目录(或保留的 --dsn 数据库)上重装,Web 登录仍是原来的;此次打印的密码无效(install 会明说)。要修改既有存储的登录,进容器跑 gitea admin user change-password(pg exec 风格:podman exec -it <container> gitea admin user change-password ...),或 --clean-data 移除后重装(SQLite 模式——dsn 模式的外置库还得自己 drop)。
重复 install 会复用运行中的容器、启动已停的容器——配置改动只有 remove + 重装或 --force 之后才生效。
数据库:SQLite 或 --dsn
默认 Gitea 用 /data 里的内嵌 SQLite——零依赖、单文件、随数据目录一起复活。要给共享或长命的部署,把元数据指到 PostgreSQL:
pgcli 在写入任何东西之前校验 DSN,并把它渲染成 GITEA__database__DB_TYPE=postgres 加 HOST/NAME/USER/PASSWD。安装摘要与 pg addon list 回显时凭据是隐藏的(只留 host:port/db)。与 PostgREST 的 --dsn 同一约定:目标库必须已存在(在受管实例上 pg exec <instance> -c 'CREATE DATABASE gitea ...',或任意其他方式建好);Gitea 首启自建自己的 schema。pg addon remove gitea——即使带 --clean-data——永远不碰外置数据库;里面只有 Gitea 的表。
Git SSH(可选)
每个 Gitea 实例都会跑一个 OpenSSH sshd——官方镜像的 s6 openssh
服务是无条件启动的(它的 run 脚本就是 exec sshd,没有任何
GITEA__server__* env 能关掉)。--ssh-port 切换的其实不是"有没有 sshd",
而是"这个 sshd 是不是活的 git-over-ssh 克隆端点"。pgcli 给每一个实例都从
gitea_ssh_start_port(基址 2222)分配一个端口——不管 SSH 开还是关——就是为了
把这个永远在跑的 sshd 挪开 22:在宿主网络上,留在 22 就会跟宿主自己的 sshd
死磕,日志会永远刷 Bind to port 22 … Address in use。pgcli 用的是镜像的
裸 env SSH_LISTEN_PORT / SSH_PORT——sshd_config 里那条 Port 行只
看这两个裸变量(只设 GITEA__server__SSH_LISTEN_PORT 仅改 Gitea 自己认为的
端口,sshd 绑的还是 22)。
- SSH 关(默认,不传
--ssh-port):DISABLE_SSH=true,Gitea 不写 git 用户的authorized_keys、也不显示ssh://clone URL。s6 的sshd照样在跑、parked 在池端口上,只是没有任何客户端会去连它。池里的端口占用 不释放——它本来就还要 park sshd。 - SSH 开(
--ssh-port N,N>0):git-over-ssh 就走这个 s6 sshd—— 即镜像的原生路线。DISABLE_SSH=false让 Gitea 给 git 用户写authorized_keys(sshd 通过AuthorizedKeysFile指向/data/gitea/ssh/读取);Gitea 自己的内建 Go SSH 监听器用START_SSH_LISTENER=false显式关掉,避免同一个 netns 里两个进程抢同一个端口;SSH_DOMAIN/SSH_PORT渲染 clone URL 为ssh://git@<host>:N/<owner>/<repo>.git。
认证走 Web UI 里给 git 账号添加的 SSH key(OS 用户是 git,Web admin 是
gitea)。host key 落在 <data>/ssh/,跨重装稳定——若用 --clean-data
移除过,客户端下次会看到新 host key 告警。
macOS bridge 下端口会额外发布,但只有 SSH 开启时才发布——parked 的
sshd 是有意不可达的,所以不给 -p。后续 install 不传 --ssh-port 会保留
已存的开关状态;--ssh-port 0 关掉克隆端点语义(端口占用不释放——
只有 DISABLE_SSH 翻转和 clone URL 变化会随 --force 重建生效)。
监听与暴露
Gitea 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以 Web UI 与 Git HTTP 在宿主每个网卡的分配端口上可达。这是有意为之——Git 服务就是给别的机器克隆的——并由自动生成的强 admin 密码加 DISABLE_REGISTRATION=true(关闭自助注册;建号请用 pg addon gitea user create)保护。想要仅回环的部署,传 --listen 127.0.0.1(用 --force 重建)。
--allow-registration 翻开的就是后一道闸:pg addon install gitea --name <n> --allow-registration --force 会渲染 DISABLE_REGISTRATION=false,Web UI 上随之出现注册入口;重新关闭要用 --allow-registration=false --force——开关是存进 pg.yaml 的配置值、只在显式传该 flag 时改写,所以普通重装(或不带 flag 的重装)会原样保留存储状态,改动也只有 --force 才推入运行中的容器。放开带来的是建号垃圾而非夺权:实测注册表单建出的账号 IsAdmin=false——Gitea 没有任何办法让注册产生管理员;提升为站点管理员只有 pg addon gitea user create --admin 或 admin 面板两条路,与账号如何产生无关。若确要在公开监听下开放注册,Gitea 同属 service 段的配套收紧项(REGISTER_EMAIL_CONFIRM、验证码、REQUIRE_SIGNIN_VIEW)不在 pgcli 渲染的 env 集合里,仍属 Gitea 侧配置。
--listen 同时也是展示用 clone URL 的主机名:通配绑定不是可连地址,所以 0.0.0.0 会显示成 localhost;显式主机/IP 则原样进入 ROOT_URL 与 ssh:// URL。当 Gitea 躲在转发了 X-Forwarded-Proto 与 Host 的反代后面时——pgcli 的 nginx 模板默认就转这两个头——Gitea 会按进来的请求重写页面显示的 clone URL,所以仓库页的克隆按钮读作 https://<代理主机>/<owner>/<repo>.git,完全正确(scheme 取自 X-Forwarded-Proto、host 取自 Host、标准端口省略)。pgcli 渲染的 ROOT_URL 仍是 http://<host>:<web端口>/(没有配置项能宣告 https/代理端口),但那只管 Gitea 服务端按 ROOT_URL 直接生成的 URL;交互式 clone URL 跟着请求走。要上 TLS,见下文「经 nginx 提供 HTTPS」一节。
经 nginx 提供 HTTPS
Gitea 自己只走明文 HTTP;要用 https:// 访问,就用 nginx 插件前置它——由 nginx 终止 TLS 再反代到 Gitea 的 Web 端口。两点关键:自带证书(或让 pgcli 签一张自签证书),以及用 nginx 的自定义 --conf-file 模式——模板默认的 client_max_body_size(1 MiB)会把稍大的 git push 用 HTTP 413 拒掉。
把代理配置写成 /srv/gitea-nginx.conf——对 Git 真正要紧的是
client_max_body_size 10m(nginx 内建默认只有 1 MiB,不抬高的话稍大的 push 还没
到 Gitea 就以 HTTP 413 死掉)和放长的 proxy_*_timeout(流式 git push 会超过
60s 默认值)。同一份文件作为通用模板样例也在 nginx 文档里(「Git HTTP 推送与
client_max_body_size」):
nginx 代理现在在 https://10.0.0.9/ 上服务,并转发到 Gitea 的 127.0.0.1:3080。用代理地址克隆与 push(把 .crt 作为 CA 交给客户端,自签证书才能通过校验):
注意:
- Gitea 的 Web 端口(3080)仍然直接开着,代理并不会把它藏起来。若只想让 nginx 能访问源站,给 Gitea 用
--listen 127.0.0.1重装让它只绑回环,并把代理 backend 指向127.0.0.1:3080(同一台宿主)——因为两者都跑在宿主网络上,这样是通的。 pg addon nginx reload不会吃到对外部挂载nginx.conf的手工改动(bind mount 钉住了原始 inode,sed -i/编辑器整体重写后容器看不到,直到重启)。reload 只应用 pgcli 自己写入的改动。手改配置后,用pg addon stop nginx --name proxy && pg addon start nginx --name proxy重启,或--force重装。- 真实 CA 证书(Let’s Encrypt / ZeroSSL / 企业 CA)才是线上环境的正解——见下文「线上域名 + 真实 CA」;它彻底免去
--cacert交接,浏览器与 git 默认可信。 - git-over-ssh(
--ssh-port)与此无关;这里的 TLS 只前置 Web 与 Git-over-HTTP。
线上域名 + 真实 CA(Let’s Encrypt / 企业 CA)
面向公网的部署不要发 pg cert 的自签叶子——给主机指一个真域名(如
git.example.com → 203.0.113.7),为它拿一张 CA 签发的证书。如何取得这张证书
不在 pgcli 的范畴内——任意 CA、任意工具都行;只要最后手里有一张 PEM 证书(含中
间链)和它的私钥即可。
然后照上文那样创建 nginx 代理,只把证书路径换成你的域名证书——同一份
--conf-file、端口、--tls 都不变:
Gitea 安装也带上 --listen git.example.com(配 --force),让 ROOT_URL 与任
何 ssh:// 提示都带上真主机名。起来后用户直接
https://git.example.com/<owner>/<repo>.git 克隆,无需任何 CA 交接——浏览器和
git 默认可信公共 CA。
记得在证书过期前更换。pgcli 在装的时候会把自带的证书对拷进插件的 TLS 目录
(<base>/addon/nginx/<name>/tls/ 下的 public.crt / private.key),而这个目
录是 bind-mount 进容器的——所以续期就是:覆盖这两个文件,然后
(已实测:reload 后所服务的证书立即更换。这行得通是因为 TLS 是目录挂载;不
像 nginx.conf 那个单文件挂载,新内容运行中的容器直接可见。)
取回 Web 登录
这个登录是 Gitea 的 Web/Git-HTTP 凭据,与任何 PostgreSQL 角色无关(即使带 --dsn)。随时可重读密码:
它也存于 pg.yaml(addons.gitea.<name>.password)。
账号管理(成员与管理员)
自助注册是关闭的,所以除安装 admin 外的每个账号都由 admin 创建。
pg addon gitea user 封装了容器内的 gitea admin user … CLI(自动以 git uid 运行),
让你在宿主上直接管账号,不必手写 podman exec。实例必须在运行。
flag(create) |
默认 | 说明 |
|---|---|---|
--username |
(必填) | 账号名 |
--email |
<username>@localhost |
需要收通知/重置密码时传真实地址 |
--password |
自动生成 | 指定登录密码,或让 pgcli 生成强密码并一次性打印 |
--admin |
false | 建站点管理员而非普通成员 |
--must-change-password |
false | Gitea 自身对"除第一个外的账号"默认 true:会强制首次登录改密,而改密前 HTTP clone/push 与 API basic-auth 一律 403。pgcli 显式发 =false,让成员建号即可克隆——只有要发一次性密码时才设 true |
这些是 Gitea 自己的账号、非 pgcli 托管,所以自动生成的密码只打印一次、
不写入 pg.yaml——建号时当场记下。(只有安装 admin 的登录会被存,那个用
pg addon password gitea 取回。)要改既有账号密码,进容器跑
gitea admin user change-password。
delete 拒删安装 admin(那是 pgcli 存的登录)。普通删除只对"不拥有任何东西"的
账号成功;若该账号还有仓库,Gitea 会以 user still has ownership of repositories
拒绝——加 --purge 才能连账号及其拥有的一切(仓库、组织、评论)一起删掉。
端口
每个 Gitea 实例占一个 Web 端口,取自以 gitea_start_port(默认 3080——Gitea 惯例的 3000 在 pgcli 里是 Grafana 池的基址,所以 Gitea 往后挪了一个池;仍然一眼认得出"Gitea 的端口")为基址的端口池,每多一个实例递增。每一个实例——SSH 开或关都一样——还从第二个池(gitea_ssh_start_port,默认 2222——Gitea 惯例的 SSH 映射)再拿一个端口用来 park 那个永远在跑的 s6 sshd 离 22;SSH 是否启用只决定那个端口是活的克隆端点还是一个没人连的空闲监听。两个池都跳过任何已被受管容器或 used_ports 列表占用的端口,分配结果写入 pg.yaml:
两个池的基址(3080 / 2222)与其他任何插件的池都不重号。
配置
配置只经环境变量(GITEA__<section>__<KEY>,外加两个裸 SSH_LISTEN_PORT /
SSH_PORT 变量——sshd 只读裸的这两个,GITEA__server__* 那份只驱动 Gitea
自己的 clone-URL 渲染)交付:镜像每次启动把它们折叠进 app.ini,/data
里那份就是被这些变量重写出来的,所以手改 app.ini 不是受支持的配置路径——
pgcli 没设的 Gitea 项虽在 app.ini 里能存活,但请自担风险。pgcli 设置的变量
有:跳过向导(INSTALL_LOCK)、监听器(HTTP_PORT/HTTP_ADDR)、展示 URL
(ROOT_URL)、注册闸门(DISABLE_REGISTRATION,两态都显式渲染——默认
true,--allow-registration 时 false;因为 Gitea 自己的默认是开,
少发一行就覆盖不掉持久 app.ini 里的旧值)、总是发出的裸
SSH_LISTEN_PORT/SSH_PORT 对(把 s6 sshd park 离 22)、DISABLE_SSH=true
(SSH 关:Gitea 不写 authorized_keys、不显示 ssh:// URL)或——SSH 开
时——DISABLE_SSH=false 加 GITEA__server__ 下渲染 URL 用的
SSH_LISTEN_PORT/SSH_PORT/SSH_DOMAIN 三件套,并显式
START_SSH_LISTENER=false 让 Gitea 自己的 Go 监听器不与 s6 sshd 在同 netns
里双绑同一端口、可选的 database 块,以及进程 uid 钉死(USER_UID/USER_GID)。
列出
默认不显示密码以保持列表可安全粘贴;pg addon list --show-password 揭示明文,pg addon password gitea 则为脚本裸打印单个密码。
启停
容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:
开机自启
要让 Gitea 实例在宿主开机时自动启动,开启它:
这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——数据目录在创建时已 chown 给 uid 1000,除 podman 本身外不需要任何宿主特权。带 --dsn 时外置 PostgreSQL 也必须在跑:有自启标记的受管实例在同一趟里排得更靠前;其他端点只需届时可达即可。完整机制、状态与关闭方式见 开机自启。
移除
rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——Gitea 以容器内 git uid(1000)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。带 --dsn 时,两种方式都绝不碰外置数据库——真要删数据得自己去 drop。
日志
排障
install失败并报 chown 诊断 —— 镜像以 uid 1000 运行且从不修复既有挂载的属主,所以 pgcli 在创建容器前先把宿主数据目录 chown 过去:rootless podman 下经podman unshare chown,rootful 宿主且 pgcli 自身以 root 运行时(sudo podman,或以 root 跑pg)直接递归chown。pgcli 是直接调用podmanCLI,所以只在这两种情形之外才会失败——例如非 root 的pg被显式指向别人的 rootful daemon。遇到时以 root 跑 install,或改用 rootless podman;单纯sudo chown -R 1000:1000 <data-dir>不能消除该错误,因为每次 install 都会重新校验属主。- 重装后登录密码不对 —— admin 账号只被创建,从不被重置。对既有存储(保留的
/data或--dsn数据库)用容器内gitea admin user change-password,或推倒重来(--clean-data;dsn 模式的外置库还要手动 drop)。 - 明明部署在服务器上,clone URL 却显示
localhost—— 通配的--listen 0.0.0.0在展示 URL 里坍缩成localhost,因为0.0.0.0不可连接。传--listen <host-or-ip>(配--force)让 clone URL 变真实。 ssh://克隆报 connection refused / 端口不对 —— 没带--ssh-port装的话克隆端点就是关的(s6 sshd 一直在跑,但只是 parked、不是 clone 端点)。而且 pgcli 让SSH_PORT(URL 里显示的)、GITEA__server__SSH_LISTEN_PORT(Gitea 自己认为的端口)和裸SSH_LISTEN_PORT(s6 sshd 实际绑的)三者保持一致——手工配出不同的值正是坏的形态。- 日志刷
Bind to port 22 … Address in use—— sshd 的 park 端口被改回 22 了(例如手改的 app.ini 或剥掉了裸 env)。一次干净的 install 不会走到这一步:pgcli 给每个实例都发出裸SSH_LISTEN_PORT=<池端口>,SSH 开或关都一样。 - HTTP
git push要密码且拒收 Web 密码 —— Gitea 要的是账号凭据;若该账号开了 2FA,HTTP push 必须改用 personal access token 或 SSH key,而不是密码。 - 端口已被分配 —— 已分配的端口记在
pg.yaml里并为后续实例跳过;先pg addon remove释放,或用--port/--ssh-port另选。
已知限制
- 镜像必须钉在版本 tag 上,升级=换
--image(Gitea 在新镜像首启时向前迁移数据库——没有回退路径)。rootful 与 rootless 构建的数据布局互不兼容:pgcli 钉的是 rootful 构建,换成-rootlesstag 读不了现有/data(也读不了别的 rootless 部署写的数据)。 - 明文 HTTP:TLS 终止与真域名是 nginx 插件的职责——用它的自定义
--conf-file模式前置 Gitea(模板默认的 1 MiBclient_max_body_size会打断稍大的git push)。Gitea 依X-Forwarded-Proto+Host重写页面 clone URL,所以在 nginx 默认转发这些头时,克隆按钮读作https://<代理>/…是对的。pgcli 把ROOT_URL渲染成http://<host>:<web端口>/,v1 没有配置项能改——目前可见的影响只是:凡 Gitea 直接依ROOT_URL(而非依请求)生成的 URL,仍用内部 http 源。 - v1 不配置 OAuth、SMTP、webhook 调优或仓库存储(MinFS)选项——上表之外的一切都是 Gitea 侧配置,pgcli 刻意不碰。自助注册是关的;建号请用
pg addon gitea user create。 - macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。