# Gitea

> 以 pgcli 插件方式运行 Gitea——自托管 Git 服务，免向导安装、自动生成 Web 登录、可选外置 PostgreSQL 元数据库

---

LLMS 索引： [llms.txt](/zh/llms.txt)

---

`pg addon install gitea` 以受管容器方式运行 [Gitea](https://about.gitea.com/)——轻量级自托管 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-net` bridge 上跑在 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、监听地址或注册开关 |

## 安装

```bash
pg addon install gitea
```

命令会按需拉取镜像、创建并启动容器，等 Gitea 首启迁移跑完，创建 admin 账号，然后打印：

```
✓ Gitea installed: "gitea"
  Container:  pgcli-gitea-gitea
  Image:      docker.io/gitea/gitea:28.1.0
  Data:       /home/you/.pgcli/addon/gitea/gitea/data
  Web URL:    http://localhost:3080/
  Git SSH:    disabled (HTTP clone/push only; enable with --ssh-port N --force)

  Login user:     gitea
  Login password: <auto-generated>
```

用浏览器打开 URL，以打印的凭据登录，在 Web UI 里建第一个 org/repo。HTTP 克隆用同一个登录：

```bash
git clone http://127.0.0.1:3080/<owner>/<repo>.git
```

## 数据目录与 `/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：

```bash
pg addon install gitea --name git1 --dsn "postgres://gitea:secret@127.0.0.1:35432/gitea"
```

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`。

```bash
pg addon install gitea --name git1 --ssh-port 2222   # 显式;不传走池分配
```

认证走 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](/zh/docs/addon/nginx/) 插件前置它——由 nginx 终止 TLS 再反代到 Gitea 的 Web 端口。两点关键：自带证书（或让 pgcli 签一张自签证书），以及用 nginx 的**自定义 `--conf-file` 模式**——模板默认的 `client_max_body_size`（1 MiB）会把稍大的 `git push` 用 HTTP 413 拒掉。

```bash
# 1. 签一张自签叶子证书，SAN 覆盖你克隆用的主机名（IP 和/或 DNS）
pg cert --host 10.0.0.9,git.internal \
        --cert-file /srv/gitea.crt --key-file /srv/gitea.key
```

把代理配置写成 `/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
worker_processes auto;
pid /tmp/nginx.pid;

events {
    worker_connections 1024;
}

http {
    # 抬高 nginx 请求体上限，避免 git push 被拒。内建默认 1 MiB——更大的请求还没
    # 到 Gitea 就以 HTTP 413 死掉。10m 能覆盖常见 push；要提交更大的二进制就继续
    # 调高（或写 0 完全不限制）。
    client_max_body_size 10m;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    upstream gitea {
        server 127.0.0.1:3080;    # pgcli 给 Gitea 分配的 Web 端口
    }

    server {
        listen 0.0.0.0:80;
        location / {
            return 308 https://$host$request_uri;   # 强制 HTTPS（308 保留方法与 body，
        }                                           # git push 能挺过重定向）
    }

    server {
        listen 0.0.0.0:443 ssl;
        # pgcli 把 --tls-cert / --tls-key 文件挂到这两个路径：
        ssl_certificate     /etc/nginx/certs/public.crt;
        ssl_certificate_key /etc/nginx/certs/private.key;
        ssl_protocols TLSv1.2 TLSv1.3;

        location / {
            proxy_pass http://gitea;
            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;   # 页面 clone URL 读成 https:// 就靠它
        }
    }
}
```

```bash
# 2. 用这份文件装代理（改动后用 --force 重建）
pg addon install nginx --name proxy --conf-file /srv/gitea-nginx.conf \
  --tls --tls-cert /srv/gitea.crt --tls-key /srv/gitea.key \
  --http-port 80 --https-port 443 --listen 0.0.0.0
```

nginx 代理现在在 `https://10.0.0.9/` 上服务，并转发到 Gitea 的 `127.0.0.1:3080`。用代理地址克隆与 push（把 `.crt` 作为 CA 交给客户端，自签证书才能通过校验）：

```bash
git clone https://10.0.0.9/<owner>/<repo>.git        # 经 TLS 代理
git -c http.sslCAInfo=/srv/gitea.crt push ...        # 信任这张自签叶子
```

注意：

- 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` 都不变：

```bash
pg addon install nginx --name proxy --conf-file /srv/gitea-nginx.conf \
  --tls \
  --tls-cert /etc/ssl/git.example.com/fullchain.pem \
  --tls-key  /etc/ssl/git.example.com/privkey.pem \
  --http-port 80 --https-port 443 --listen 0.0.0.0
```

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 进容器的——所以续期就是：覆盖这两个文件，然后

```bash
pg addon nginx reload --name proxy
```

（已实测：reload 后所服务的证书立即更换。这行得通是因为 TLS 是**目录**挂载；不
像 `nginx.conf` 那个单文件挂载，新内容运行中的容器直接可见。）


## 取回 Web 登录

这个登录是 Gitea 的 Web/Git-HTTP 凭据，与任何 PostgreSQL 角色无关（即使带 `--dsn`）。随时可重读密码：

```bash
pg addon password gitea              # 裸打印到 stdout
pg addon password gitea --file ./pw  # 或写入 0600 文件
```

它也存于 `pg.yaml`（`addons.gitea.<name>.password`）。

## 账号管理（成员与管理员）

自助注册是关闭的，所以除安装 admin 外的每个账号都由 admin 创建。
`pg addon gitea user` 封装了容器内的 `gitea admin user …` CLI（自动以 git uid 运行），
让你在宿主上直接管账号，不必手写 `podman exec`。实例必须在运行。

```bash
# 成员，密码自动生成（一次性打印，且不写入 pg.yaml）
pg addon gitea user create --username alice --email alice@corp.example
# 成员，指定密码
pg addon gitea user create --username bob   --password 'B0b-pin-pass'
# 再建一个站点管理员
pg addon gitea user create --username carol --admin
# 列出全部账号
pg addon gitea user list
# 删除账号（须不拥有任何仓库，否则用 --purge）……
pg addon gitea user delete --username bob
# ……或连账号拥有的一切一起清掉
pg addon gitea user delete --username bob --purge
```

| 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`：

```bash
pg addon install gitea --name git2                 # 第二个实例：下一个空闲 Web 端口
pg addon install gitea --name git3 --port 3100     # 显式端口优先
pg addon install gitea --name git4 --ssh-port 0    # SSH 关掉，parked sshd 仍占池端口
```

两个池的基址（3080 / 2222）与其他任何插件的池都不重号。

## 配置

```yaml
addons:
  gitea:
    git1:
      name: git1
      image_tag: docker.io/gitea/gitea:28.1.0
      host_port: 3080
      listen: 0.0.0.0
      admin_user: gitea
      password: <generated>
      ssh: true
      ssh_port: 2222
      dsn: postgres://gitea:secret@127.0.0.1:35432/gitea
      autostart: false
      # data_dir: /srv/gitea-data    # 可选，覆盖默认位置
```

配置只经环境变量（`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`）。

### 列出

```bash
pg addon list
```

```
Web add-ons (gitea):
  gitea (name: git1)
    Status:      running
    Web URL:     http://localhost:3080/
    Git SSH:     ssh://git@localhost:2222/
    Database:    external PostgreSQL (127.0.0.1:35432/gitea)
    Login user:  gitea
    Data:        /home/you/.pgcli/addon/gitea/git1/data
    Image:       docker.io/gitea/gitea:28.1.0
    Container:   pgcli-gitea-git1
```

默认不显示密码以保持列表可安全粘贴；`pg addon list --show-password` 揭示明文，`pg addon password gitea` 则为脚本裸打印单个密码。

## 启停

容器以 podman 的 `unless-stopped` 重启策略运行，宿主重启后随 podman 服务自动回来。`start`/`stop` 显式管理它们：

```bash
pg addon stop gitea
pg addon start gitea
```

## 开机自启

要让 Gitea 实例在宿主开机时自动启动，开启它：

```bash
pg autostart enable --gitea --name git1
```

这会在 `pg.yaml` 里给实例打标记，并安装/刷新一个开机服务（Linux 上是 systemd `--user` unit，macOS 上是 LaunchAgent）来执行 `pg start --autostart`。开机启动只负责把容器拉起来——数据目录在创建时已 chown 给 uid 1000，除 podman 本身外不需要任何宿主特权。带 `--dsn` 时外置 PostgreSQL 也必须在跑：有自启标记的受管实例在同一趟里排得更靠前；其他端点只需届时可达即可。完整机制、状态与关闭方式见 [开机自启](/zh/docs/autostart/)。

## 移除

```bash
pg addon remove gitea              # 保留数据目录——仓库与登录跨重装存活
pg addon remove gitea --clean-data # 连数据目录一起删（仓库就没了）
```

rootless podman 下，`--clean-data` 经 `podman unshare rm` 删除数据目录——Gitea 以容器内 `git` uid（1000）写文件，它映射到宿主的 subordinate uid，宿主用户无法直接 `rm`。带 `--dsn` 时，两种方式都绝不碰外置数据库——真要删数据得自己去 drop。

## 日志

```bash
pg logs addon gitea            # 最近的控制台输出
pg logs addon gitea -f         # 跟踪
pg logs addon gitea -n 200     # 最近 200 行（也支持 -i / --tail）
```

## 排障

- **`install` 失败并报 chown 诊断** —— 镜像以 uid 1000 运行且从不修复既有挂载的属主，所以 pgcli 在创建容器前先把宿主数据目录 chown 过去：**rootless** podman 下经 `podman unshare chown`，**rootful** 宿主且 pgcli 自身以 root 运行时（`sudo podman`，或以 root 跑 `pg`）直接递归 `chown`。pgcli 是直接调用 `podman` CLI，所以只在这两种情形之外才会失败——例如非 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 构建，换成 `-rootless` tag 读不了现有 `/data`（也读不了别的 rootless 部署写的数据）。
- 明文 HTTP：TLS 终止与真域名是 [nginx](/zh/docs/addon/nginx/) 插件的职责——用它的**自定义 `--conf-file` 模式**前置 Gitea（模板默认的 1 MiB `client_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 上验证过。

---

反链：

- [插件 (Addons)](/zh/docs/addon/)
- [平台支持](/zh/docs/platform/)
