# Gitea

> Run Gitea — self-hosted Git service — as a managed container, with a no-wizard install and an auto-generated web login.

---

LLMS index: [llms.txt](/llms.txt)

---

`pg addon install gitea` runs [Gitea](https://about.gitea.com/) — the
lightweight self-hosted Git service — as a managed container from the official
upstream image (`docker.io/gitea/gitea`, rootful build). Like Grafana it is a
top-level addon: it is not attached to any one PostgreSQL instance — it fronts
none, though it can *use* one as its metadata database (see `--dsn` below).
One container serves the web UI, the Git HTTP traffic, and git-over-ssh on the
image's OpenSSH `sshd` (see [Git SSH](#git-ssh-optional) — that `sshd` runs on
every instance whether or not `--ssh-port` is set; the flag only makes it the
*live* clone endpoint).

The install is fully non-interactive: `INSTALL_LOCK=true` skips Gitea's web setup wizard, and the admin account is created afterwards via the in-container `gitea admin user create` CLI. The web login password is generated on first install, printed once, and stored in `pg.yaml`; retrieve it later with `pg addon password gitea`. `Listen` defaults to `0.0.0.0` — a Git host is cloned from other machines — and the exposure is guarded by the generated strong password plus closed self-registration; pass `--listen 127.0.0.1` for loopback-only access.

> **Platform**: Linux is the tested platform. The design supports macOS (the
> image is multi-arch; the container runs under the podman machine VM on the
> `pgcli-net` bridge with the web port — and, when enabled, the SSH port —
> published), but it has not been validated there yet.

## Parameters

| Flag | Default | Description |
|---|---|---|
| `--name` | `gitea` | Instance name (map key) |
| `--image` | `docker.io/gitea/gitea:28.1.0` | Image override (rootful build only — see Known limitations) |
| `--port` | auto (base 3080) | Host web port; auto-allocated from `gitea_start_port` |
| `--ssh-port` | unset | Make the always-running sshd the **live** git-over-ssh endpoint on this host port (see [Git SSH](#git-ssh-optional)). A port is allocated from `gitea_ssh_start_port` (base 2222) for **every** instance to park sshd off 22, SSH on or off; `--ssh-port 0` disables *live* cloning but keeps the reservation |
| `--listen` | `0.0.0.0` | Bind address — defaults to every interface; use `127.0.0.1` for loopback-only. Also the host Gitea renders into clone URLs |
| `--admin-user` | `gitea` | Web login user (deliberately not `git` — that is the container's OS user `ssh://` clones authenticate *as*) |
| `--password` | auto-generated | Web login password (applied only when the admin account does not yet exist — see below) |
| `--dsn` | empty | `postgres://user:pass@host:port/db` to keep Gitea's metadata in an external PostgreSQL instead of the embedded SQLite; the database must already exist |
| `--allow-registration` | false | Open self-service signup (the web UI shows a Register entry). Closed by default — see [Listening and exposure](#listening-and-exposure) |
| `--data-dir` | auto | Data directory override (default under the base dir) |
| `--force` | false | Recreate even when running — to apply changed ports, credentials, dsn, listen address or the registration toggle |

## Install

```bash
pg addon install gitea
```

The command pulls the image if needed, creates and starts the container, waits for Gitea's first-boot migrations to finish, creates the admin account, and prints:

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

Open the URL, sign in with the printed credentials, and create your first org/repo in the web UI. Clone over HTTP with the same login:

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

## The data directory and `/data`

Everything Gitea owns lives in one bind-mounted directory: the repositories, the rendered `/data/gitea/conf/app.ini`, the SQLite metadata database (`/data/gitea/gitea.db` — absent with `--dsn`), the SSH host keys, and logs. It survives `pg addon remove` (delete it with `--clean-data`), so reinstalling under the same name revives everything.

The image's rootful build runs as the `git` user — uid/gid **1000**, pinned via `USER_UID`/`USER_GID` so the host-side pre-chown and the container process can never drift apart — and its entrypoint only chowns what it creates itself, so pgcli chowns the data dir on the host before creating the container: via `podman unshare chown 1000:1000` under rootless podman, or a direct recursive `chown` when pgcli runs as root on a rootful host.

**pgcli never injects `SECRET_KEY` or `INTERNAL_TOKEN`.** Gitea folds every `GITEA__<section>__<KEY>` environment variable into the *persisted* `app.ini` on each boot; if pgcli regenerated and injected crypto secrets, a reinstall over a kept data dir would re-key the persisted `app.ini` while the surviving database still holds 2FA seeds and OAuth client secrets encrypted under the *old* key — a silent, unlogged lockout. So those keys belong to the volume: Gitea generates them itself on first boot, and pgcli owns only the admin login password.

The admin password takes effect **only when the admin account does not exist** in the database — it is created with `gitea admin user create`, never reset. Reinstalling over a kept data dir (or a kept `--dsn` database) keeps the original login; the printed password is then inert (install says so explicitly). To change the login of an existing store, run `gitea admin user change-password` inside the container (`pg exec <instance> -- ...` style: `podman exec -it <container> gitea admin user change-password ...`), or remove with `--clean-data` and reinstall (SQLite mode — a `--dsn` database also has to be dropped by hand).

Re-running `install` reuses a running container and starts a stopped one — config changes only apply after a `remove` + reinstall or `--force`.

## Database: SQLite or `--dsn`

By default Gitea uses its embedded SQLite database inside `/data` — zero dependencies, one file, revived with the data dir. For anything shared or long-lived, point the metadata at PostgreSQL:

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

pgcli validates the DSN before writing anything and renders it into `GITEA__database__DB_TYPE=postgres` plus `HOST`/`NAME`/`USER`/`PASSWD`. The install summary and `pg addon list` echo it back with the credentials hidden (`host:port/db`). Same contract as PostgREST's `--dsn`: **the database must already exist** (create it on a managed instance with `pg exec <instance> -c 'CREATE DATABASE gitea ...'` or any other way); Gitea creates its own schema on first boot. `pg addon remove gitea` — even with `--clean-data` — never touches the external database; it only holds Gitea's tables.

## Git SSH (optional)

**Every Gitea instance runs an OpenSSH `sshd` — the official image's s6
`openssh` service is unconditional** (its `run` script `exec`s sshd; no
`GITEA__server__*` env turns it off). What `--ssh-port` actually toggles is
whether that sshd is the **live git-over-ssh clone endpoint** or just an idle
listener. `--ssh-port` takes a host port from `gitea_ssh_start_port` (base
`2222`) for *every* instance — SSH on or off — precisely so the always-on
sshd is **parked off 22**: on the host network, leaving it on 22 would race
the host's own sshd and the log would fill with `Bind to port 22 … Address in
use` forever. pgcli moves it by setting the image's **bare** `SSH_LISTEN_PORT`
/ `SSH_PORT` env (those are what the rendered `sshd_config`'s `Port` line
actually consumes — `GITEA__server__SSH_LISTEN_PORT` alone only changes
what Gitea *believes* the port is, not what sshd binds).

- **SSH off (default, no `--ssh-port`)**: `DISABLE_SSH=true` so Gitea neither
  writes the git user's `authorized_keys` nor advertises `ssh://` clone URLs.
  The s6 `sshd` is still running, parked on a pool port, and no client will
  ever reach it. The pool reservation stays — nothing frees it.
- **SSH on (`--ssh-port N`, N>0)**: git-over-ssh rides that same s6 sshd — the
  image's native route. `DISABLE_SSH=false` makes Gitea populate the git
  user's `authorized_keys` (which sshd reads via `AuthorizedKeysFile`
  pointed into `/data/gitea/ssh/`); Gitea's **own built-in Go SSH listener**
  is pinned off with `START_SSH_LISTENER=false` so it does not double-bind
  the same port in the same netns; and `SSH_DOMAIN` / `SSH_PORT` render the
  `ssh://git@<host>:N/<owner>/<repo>.git` clone URL.

```bash
pg addon install gitea --name git1 --ssh-port 2222   # explicit, or omit for pool
```

Auth is the **git account's** SSH keys added through the web UI (the OS user
is `git`; the web admin is `gitea`). Host keys live under
`<data>/ssh/`, so they survive reinstalls — if you remove with
`--clean-data`, clients see a new-host-key warning the next time.

Under the macOS bridge the SSH port is additionally published, but only when
SSH is **on** — a parked sshd is unreachable by design, so it gets no `-p`.
Not passing `--ssh-port` on a later install keeps the stored setting;
`--ssh-port 0` disables *live* cloning (the port reservation stays — only the
`DISABLE_SSH` flip and the clone-URL change take effect, with a `--force`
recreate).

## Listening and exposure

Gitea's `Listen` defaults to `0.0.0.0`: the container shares the host's network namespace, so the web UI and Git HTTP are reachable on every interface at the assigned port. This is deliberate — a Git host is cloned from other machines — and is protected by the generated strong admin password plus `DISABLE_REGISTRATION=true` (self-service account creation is off; create accounts with `pg addon gitea user create`). For a loopback-only deployment pass `--listen 127.0.0.1` (recreate with `--force`).

`--allow-registration` flips that second guard: `pg addon install gitea --name <n> --allow-registration --force` renders `DISABLE_REGISTRATION=false` and the web UI grows a Register entry. Close it again with `--allow-registration=false --force` — the toggle is a stored config value gated on the flag being *passed*, so a plain re-install (or one without the flag) keeps whatever state is stored, and only `--force` pushes a change into the live container. What opens up is account *spam*, not takeover: a field test confirmed the register form creates a **NORMAL user** (`IsAdmin=false`) — Gitea has no way to make signup produce an admin; elevation is exclusively `pg addon gitea user create --admin` or the admin panel, independent of how the account was born. Companion Gitea-side hardening for an open-registration site lives in the same `service` section (`REGISTER_EMAIL_CONFIRM`, CAPTCHA, `REQUIRE_SIGNIN_VIEW`) — outside pgcli's rendered env set, so it stays Gitea-side configuration.

`--listen` is also what displayed clone URLs use: a wildcard bind is not a connectable address, so it is echoed as `localhost`, and any explicit host/IP appears verbatim in `ROOT_URL` and the `ssh://` URLs. When Gitea sits behind a reverse proxy that forwards `X-Forwarded-Proto` and `Host` — which pgcli's nginx template does by default — Gitea **rewrites the displayed clone URL to match the incoming request**, so the code button on the repo page reads `https://<proxy-host>/<owner>/<repo>.git` correctly (scheme from `X-Forwarded-Proto`, host from `Host`, standard port dropped). pgcli's rendered `ROOT_URL` stays `http://<host>:<web-port>/` because there is no config face to advertise `https`/the proxy port — but that only governs webhook/redirect URLs generated *server-side*; interactive clone URLs follow the request. For TLS, see [HTTPS via nginx](#https-via-nginx).

## HTTPS via nginx

Gitea serves plaintext HTTP; to reach it over `https://` you front it with the
[nginx](/docs/addon/nginx/) addon, which terminates TLS and proxies to the
Gitea web port. Two things to get right: bring your own certificate (or let
pgcli mint a self-signed one), and use nginx's **custom `--conf-file` mode** —
the template's default `client_max_body_size` (1 MiB) rejects any larger
`git push` with HTTP 413.

```bash
# 1. a self-signed leaf whose SAN covers the host you clone by (IP and/or DNS)
pg cert --host 10.0.0.9,git.internal \
        --cert-file /srv/gitea.crt --key-file /srv/gitea.key
```

Write the proxy config as `/srv/gitea-nginx.conf` — the parts that matter for
Git are `client_max_body_size 10m` (nginx's built-in default is 1 MiB, and
without raising it a larger `git push` dies with HTTP 413 before reaching
Gitea) and the long `proxy_*_timeout` (streaming `git push` outlives the 60s
default). The same file as a generic template also lives in the nginx docs
("Git HTTP push with `client_max_body_size`"):

```nginx
worker_processes auto;
pid /tmp/nginx.pid;

events {
    worker_connections 1024;
}

http {
    # Lift nginx's request-body cap so git pushes aren't rejected. nginx's
    # built-in default is 1 MiB — anything larger dies with HTTP 413 before
    # reaching Gitea. 10m covers typical pushes; raise it (or use 0 for no
    # limit) if you commit larger binaries.
    client_max_body_size 10m;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    upstream gitea {
        server 127.0.0.1:3080;    # the Gitea web port pgcli allocated
    }

    server {
        listen 0.0.0.0:80;
        location / {
            return 308 https://$host$request_uri;   # force HTTPS (308 keeps method+body,
        }                                           # so git push survives the redirect)
    }

    server {
        listen 0.0.0.0:443 ssl;
        # pgcli bind-mounts the --tls-cert / --tls-key files at these paths:
        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;   # what makes the displayed
        }                                                 # clone URL read https://…
    }
}
```

```bash
# 2. install the proxy on that file (recreate anytime with --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
```

The nginx proxy now serves `https://10.0.0.9/` and forwards to Gitea's
`127.0.0.1:3080`. Clone and push over the proxy URL (hand clients the `.crt` as
their CA so the self-signed cert verifies):

```bash
git clone https://10.0.0.9/<owner>/<repo>.git        # over the TLS proxy
git -c http.sslCAInfo=/srv/gitea.crt push ...        # trust the self-signed leaf
```

Notes:

- The Gitea web port (3080) is still open directly; the proxy does not hide it.
  For an origin that only nginx may reach, reinstall Gitea with
  `--listen 127.0.0.1` so it binds loopback only, and point the proxy's backend
  at `127.0.0.1:3080` (same host) — works because both run on the host network.
- `pg addon nginx reload` will **not** pick up an external edit to the mounted
  `nginx.conf` (the bind mount pins the original inode, so a `sed -i`/editor
  rewrite is invisible until the container restarts). Reload only applies
  changes pgcli itself wrote. After hand-editing the conf, restart with
  `pg addon stop nginx --name proxy && pg addon start nginx --name proxy`, or
  reinstall with `--force`.
- A **real CA cert** (Let's Encrypt / ZeroSSL / corporate) is the right choice in
  production — see *Production domain + a real CA* below; it drops the
  `--cacert` hand-off entirely, browsers and git trust it out of the box.
- git-over-ssh (`--ssh-port`) is independent of this; TLS here only fronts the
  web UI and Git-over-HTTP.

### Production domain + a real CA (Let's Encrypt / corporate)

For anything facing the internet, don't ship a `pg cert` self-signed leaf —
point a real domain at the host (e.g. `git.example.com → 203.0.113.7`) and get a
CA-signed certificate for it. How you obtain that certificate is outside
pgcli's scope — any CA and any tool you like; all that matters is ending up
with a PEM cert (including its intermediate chain) and its private key.

Then create the nginx proxy exactly as in the steps above, **only swapping the
certificate paths** for your domain cert — the same `--conf-file`, ports and
`--tls` are unchanged:

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

Set `--listen git.example.com` on the Gitea install too (with `--force`), so
`ROOT_URL` and any `ssh://` hints carry the real hostname. Once it's up, users
clone `https://git.example.com/<owner>/<repo>.git` with no CA hand-off —
browsers and `git` trust public CAs by default.

Remember to replace the certificate before it expires. pgcli copies the BYO
pair into the addon's TLS dir (`<base>/addon/nginx/<name>/tls/` as
`public.crt` / `private.key`) at install, and that directory is bind-mounted
into the container — so renewal is: overwrite those two files, then

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

(Verified: the served certificate changes immediately after the reload. This
works because the TLS dir is mounted as a directory; unlike the single-file
`nginx.conf` mount, new contents are visible to the running container.)


## Retrieving the web login

The login is Gitea's web/Git-HTTP sign-in, unrelated to any PostgreSQL role (even with `--dsn`). Re-read the password at any time:

```bash
pg addon password gitea              # bare password on stdout
pg addon password gitea --file ./pw  # or write it to a 0600 file
```

It is also stored in `pg.yaml` (`addons.gitea.<name>.password`).

## Managing accounts (members and admins)

Self-registration is closed, so every account beyond the install admin is created
by an admin. `pg addon gitea user` wraps the in-container `gitea admin user …`
CLI (run as the git uid automatically) so you manage accounts from the host
without a manual `podman exec`. The instance must be running.

```bash
# a member, generated password (printed once, NOT stored in pg.yaml)
pg addon gitea user create --username alice --email alice@corp.example
# a member, pinned password
pg addon gitea user create --username bob   --password 'B0b-pin-pass'
# a second site admin
pg addon gitea user create --username carol --admin
# list everyone
pg addon gitea user list
# remove an account (must own nothing, or use --purge) ...
pg addon gitea user delete --username bob
# ... or drop everything the account owns
pg addon gitea user delete --username bob --purge
```

| flag (`create`) | default | meaning |
|---|---|---|
| `--username` | (required) | the account name |
| `--email` | `<username>@localhost` | pass a real address when the user should get notifications / password resets |
| `--password` | generated | pin the login, or let pgcli generate a strong one and print it once |
| `--admin` | false | create a site admin instead of a normal member |
| `--must-change-password` | **false** | Gitea's own default is true for every account after the first; that forces a change on first login and, until it happens, returns **403 for HTTP clone/push and API basic-auth**. pgcli sends `=false` explicitly so a member can clone immediately — set it true only to hand out a one-time password |

These logins are Gitea's own accounts, not pgcli-managed, so a **generated**
password is printed once and is **not written to `pg.yaml`** — record it at
creation. (Only the install admin's login is stored; that one you retrieve with
`pg addon password gitea`.) To change an existing account's password, run
`gitea admin user change-password` inside the container.

`delete` refuses to remove the install admin (it is pgcli's stored login). A
plain delete only succeeds for an account that owns nothing; if it still owns
repositories Gitea refuses with `user still has ownership of repositories` —
pass `--purge` to remove the account *and* everything it owns (repos, orgs,
comments).

## Ports

Each Gitea instance takes one port for the web UI, allocated from a pool with base `gitea_start_port` (default `3080` — Gitea's conventional 3000 is the Grafana pool's base in pgcli, so Gitea starts one pool over; still recognizably "the Gitea port"). Every instance *also* takes a port from a second pool (`gitea_ssh_start_port`, default `2222` — the conventional Gitea SSH mapping) to park the image's always-running sshd off 22; whether SSH is on or off only decides whether that port is a live clone endpoint or an idle listener, so the reservation is held either way. Both pools skip ports already taken by any managed container or the `used_ports` list, and the allocations are written into `pg.yaml`:

```bash
pg addon install gitea --name git2                 # second instance: next free web port
pg addon install gitea --name git3 --port 3100     # explicit port still wins
pg addon install gitea --name git4 --ssh-port 0    # SSH disabled: parked sshd keeps its pool port
```

The two pools' bases (3080 / 2222) are disjoint from every other addon's pool.

## Configuration

```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    # optional, overrides the default location
```

Config is env-only (`GITEA__<section>__<KEY>` plus two **bare**
`SSH_LISTEN_PORT` / `SSH_PORT` vars — those are what the image's always-running
sshd actually binds; the `GITEA__server__*` copy only drives Gitea's own
SSH-aware clone-URL rendering), folded by the image into `app.ini` on every
boot; the `app.ini` inside `/data` is rewritten from these variables, so
hand-editing it is not a supported configuration path (settings pgcli does not
set survive the rewrite, but changing one of pgcli's own keys by hand just gets
undone on the next boot). The variables pgcli sets are: the wizard skip
(`INSTALL_LOCK`), the listener (`HTTP_PORT`/`HTTP_ADDR`), the display URL
(`ROOT_URL`), the registration gate (`DISABLE_REGISTRATION`, rendered in both
states — `true` by default, `false` with `--allow-registration`, since Gitea's
own default is *open* and an omitted line could not override a persisted one),
the always-emitted
**bare** `SSH_LISTEN_PORT`/`SSH_PORT` pair that parks the s6 sshd off 22,
`DISABLE_SSH=true` when SSH is off (Gitea then also does not write
`authorized_keys` or show ssh:// URLs) or — when SSH is on — `DISABLE_SSH=false`
plus the URL-rendering trio `SSH_LISTEN_PORT`/`SSH_PORT`/`SSH_DOMAIN` under
`GITEA__server__` **and** `START_SSH_LISTENER=false` so Gitea's own Go listener
does not double-bind the same port, the optional database block, and the process
uid pin (`USER_UID`/`USER_GID`).

### List

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

Passwords are hidden by default to keep the listing paste-safe; `pg addon list --show-password` reveals the plaintext value, and `pg addon password gitea` prints a single one bare for scripting.

## Start and stop

Containers run with podman's `unless-stopped` restart policy, so they come back automatically with the podman service after a host reboot. `start`/`stop` manage them explicitly:

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

## Auto-start on Boot

To have a Gitea instance started automatically on host boot, enable it:

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

This flags the instance in `pg.yaml` and installs/refreshes a boot service (a systemd `--user` unit on Linux, a LaunchAgent on macOS) that runs `pg start --autostart`. A boot-time start only brings the container up — the data dir was already chowned to uid 1000 at create time, so it needs no host privileges beyond podman itself. With `--dsn`, the external PostgreSQL must be up too: a managed instance with its own autostart starts earlier in the same pass; any other endpoint just needs to be reachable. See [Autostart](/docs/autostart/) for the full mechanism, status and disabling.

## Remove

```bash
pg addon remove gitea              # keeps the data dir — repos and login survive a reinstall
pg addon remove gitea --clean-data # deletes the data dir too (repositories are gone)
```

Under rootless podman, `--clean-data` deletes the data dir through `podman unshare rm` — Gitea writes as the container's `git` uid (1000), which maps to a subordinate uid the host user cannot `rm` directly. With `--dsn` the external database is never touched either way — drop it yourself if you really want the data gone.

## Logs

```bash
pg logs addon gitea            # show recent console output
pg logs addon gitea -f         # follow
pg logs addon gitea -n 200     # last 200 lines (also -i / --tail)
```

## Troubleshooting

- **`install` fails with a chown diagnostic** — the image runs as uid 1000 and never fixes ownership of an existing mount, so pgcli chowns the host data dir before creating the container: through `podman unshare chown` under **rootless** podman, or a direct recursive `chown` when pgcli itself runs as root on a **rootful** host. The manager shells out to the plain `podman` CLI, so this only fails outside those two cases — e.g. a non-root `pg` explicitly pointed at someone else's rootful daemon. Run the install as root, or switch to rootless podman; a manual `sudo chown -R 1000:1000 <data-dir>` alone does not clear it, since pgcli re-checks ownership on every install attempt.
- **The login password doesn't work after a reinstall** — the admin account is *created*, never reset. On an existing store (kept `/data` or `--dsn` database) use `gitea admin user change-password` inside the container, or wipe and start over (`--clean-data`, and drop the external DB by hand in dsn mode).
- **Clone URLs show `localhost` even though Gitea is on a server** — a wildcard `--listen 0.0.0.0` collapses to `localhost` in displayed URLs because `0.0.0.0` is not connectable. Pass `--listen <host-or-ip>` (with `--force`) to make clone URLs real.
- **`ssh://` clone says connection refused / wrong port** — *live* cloning is off unless the instance was installed with `--ssh-port` (the s6 sshd is running but parked, so it is not a clone endpoint). And pgcli keeps `SSH_PORT` (what URLs show), the Gitea-side `SSH_LISTEN_PORT`, and the **bare** `SSH_LISTEN_PORT` (what the s6 sshd actually binds) all equal — a hand-mismatched config breaks exactly this way.
- **Log line `Bind to port 22 … Address in use`** — the sshd's park port has somehow reverted to 22 (an app.ini edit survived a boot, or bare env was stripped). A fresh install never hits this: pgcli always emits bare `SSH_LISTEN_PORT=<pool port>` for every instance, SSH on or off.
- **`git push` over HTTP asks for a password and rejects the web one** — Gitea asks for the account's credentials; if the account has 2FA on, HTTP push needs a personal access token or an SSH key instead of the password.
- **Port already allocated** — the assigned ports are held in `pg.yaml` and skipped for later instances; free them with `pg addon remove` first, or pick others with `--port` / `--ssh-port`.

## Known limitations

- The image must be pinned to a version tag and upgraded by changing `--image` (Gitea migrates its database forward on first boot of the newer image — there is no downgrade path). **Rootful and rootless builds have mutually incompatible data layouts**: pgcli pins the rootful build and an `-rootless` tag will not read the existing `/data` (or the data written by another rootless deployment).
- Plaintext HTTP: TLS termination and real hostnames are the [nginx](/docs/addon/nginx/) addon's job — front Gitea with it in **custom `--conf-file` mode** (the template's 1 MiB `client_max_body_size` default breaks larger `git push`). Gitea rewrites the on-page clone URL from `X-Forwarded-Proto` + `Host`, so behind nginx's default proxy headers the copy button reads `https://<proxy>/…` correctly. pgcli renders `ROOT_URL` as `http://<host>:<web-port>/` and v1 exposes no knob to change that — the only visible effect so far is anything Gitea generates from `ROOT_URL` directly (rather than from the request) keeps the internal http origin.
- v1 sets no OAuth, SMTP, webhook-tuning, or repository-storage (MinFS) options — everything beyond the table above is Gitea-side configuration pgcli deliberately leaves alone. Self-registration is closed; create users with `pg addon gitea user create`.
- On macOS the container runs inside the podman machine; it has been validated on Linux only.

---

Backlinks:

- [Addons](/docs/addon/)
- [Platform Support](/docs/platform/)
