homelab-blueprint/docker-compose-files/forgejo/README.md
Brian Pooe fef521909b feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access
Public repos are now browsable/clonable anonymously (HTTPS) and over SSH,
proxied through the Hetzner VPS. Unlike other public apps this is
deliberately not behind the oauth2-proxy/Zitadel gate, since Forgejo
already enforces its own per-repo visibility. Root redirects to the
.profile landing page. Documents the REQUIRE_SIGNIN_VIEW gotcha and the
Zitadel OIDC redirect_uri fix needed whenever FORGEJO_DOMAIN changes.
2026-07-19 18:34:13 +02:00

172 lines
7.7 KiB
Markdown

# Forgejo
Self-hosted Git service (soft fork of Gitea) with Postgres.
## Persistent Data
Split between local disk and the UNAS `forgejo_data` share (see
[nfs-mounts](../../docs/unas/nfs-mounts.md) — you'll need to add this share
following that guide, it isn't in the table yet):
- `forgejo_data` named volume (local disk) — app config, SSH host keys,
session store, Bleve search index.
- `forgejo_db_data` named volume (local disk) — Postgres data directory.
Databases don't belong on NFS regardless of engine (locking/fsync
semantics), so this stays local even though Forgejo isn't using SQLite.
- Share dirs, NFS-mounted on the Proxmox host and bind-mounted into the LXC
at `FORGEJO_DATA_ROOT`:
- `repositories/` — bare git repo objects. This is what grows over time.
- `lfs/` — Git LFS objects.
- `attachments/` — issue/PR attachments.
- `avatars/` — user and repo avatars.
Git's own protocol tolerates NFS latency far better than a database does,
so this bulk data is a safe, capacity-friendly NAS candidate.
The share binds use `create_host_path: false`, so the stack refuses to start
if the share is not mounted, rather than silently writing into an empty
directory that later needs merging back in.
## Installation
1. Create `.env` if it does not exist:
```bash
cp -n docker-compose-files/forgejo/.env.sample docker-compose-files/forgejo/.env
```
2. Set the required values in `docker-compose-files/forgejo/.env` — at
minimum `FORGEJO_ROOT_URL`, `FORGEJO_DOMAIN`, `FORGEJO_DB_PASSWORD`, and
`FORGEJO_DATA_ROOT`.
3. Add the UNAS share and bind-mount it into the LXC, following
[nfs-mounts.md](../../docs/unas/nfs-mounts.md) (add `forgejo_data` to the
share list there, same `No Root Squash` treatment as `paperless_data`):
```bash
# On the Proxmox host, once the share is mounted at /mnt/unas/forgejo_data
pct set <vmid> -mp0 /mnt/unas/forgejo_data,mp=/mnt/unas/forgejo_data
mkdir -p /mnt/unas/forgejo_data/{repositories,lfs,attachments,avatars}
chown -R 101000:101000 /mnt/unas/forgejo_data
```
4. Deploy with `make`:
```bash
make deploy-forgejo
```
Manual equivalent:
```bash
./substitute_env.sh docker-compose-files/forgejo/template.yaml docker-compose.forgejo.yml .env docker-compose-files/forgejo/.env
docker compose -f docker-compose.forgejo.yml config --quiet
docker compose -f docker-compose.forgejo.yml up -d
```
5. Open `http://<host-ip>:3000`, create the first (admin) account, and
confirm repo creation/push/clone works.
## Reverse Proxy
Add a block for `git.<domain>` (port `FORGEJO_HTTP_PORT`) to
`docker-compose-files/caddy/Caddyfile_template`, then `make reload-caddy`.
Git-over-SSH (`FORGEJO_SSH_PORT`) is not proxyable through Caddy — expose that
port directly if you want SSH clone/push from outside the LAN.
`FORGEJO_SSH_PORT` defaults to `2222` because the LXC's own admin sshd
already owns port 22. This instance was later moved to the standard port —
the LXC's admin sshd got relocated to 2200 first (systemd `ssh.socket`
override), freeing 22 for Forgejo's git-SSH. See `.env.sample` for the
one-time steps if you want to do the same elsewhere.
Since SSH bypasses Caddy, this instance instead gets a plain Technitium `A`
record — `forgejo.brianpooe.com` → `10.0.10.18` — so clone URLs read
`ssh://git@forgejo.brianpooe.com/<owner>/<repo>.git` instead of the raw
LXC IP. Add this in the Technitium admin UI (or via its API) as a normal
zone record; it isn't behind the `*.home.brianpooe.com` → Caddy wildcard.
## Public Exposure
This instance is publicly reachable at `git.brianpooe.com` (HTTPS) and
`ssh://git@forgejo.brianpooe.com` (SSH), proxied through the Hetzner VPS —
see `~/netbird-vps/docs/netbird-acl-playbook.md` and `public-app-sso.md` on
the VPS for the edge side. Unlike other public apps (koffan, IT-Tools), this
one is deliberately **not** behind the oauth2-proxy/Zitadel SSO gate: public
repos need to be browsable/clonable with no login, like GitHub. Forgejo
enforces its own per-repo visibility regardless of network path, so private
repos stay private without an extra gate.
One canonical hostname everywhere — `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` are
`git.brianpooe.com`, not a `.home.` subdomain — resolved via split-horizon
DNS: Technitium answers it directly on the LAN (own zone, `A` record ->
`10.0.15.5`), public DNS (`*.brianpooe.com` wildcard) sends it to the VPS.
Same pattern for `forgejo.brianpooe.com` (`A` record -> `10.0.10.18`
directly, bypassing Caddy, since SSH can't be proxied).
Two settings matter for a public instance and are easy to get wrong:
- `FORGEJO_REQUIRE_SIGNIN_VIEW` **must be `false`**. When `true` it forces
every page (including `/explore` and the `.profile` landing page) through
`/user/login` for anonymous visitors, regardless of `LANDING_PAGE` — it
silently defeats "outsiders can browse public repos."
- `FORGEJO_LANDING_PAGE=explore` so anonymous visitors land on the public
repo listing instead of a login page. On top of that, the Caddy vhost for
`git.brianpooe.com` (`docker-compose-files/caddy/Caddyfile_template`) has
`redir / /brianpooe 302`, so the *root* domain goes straight to the
`.profile` landing page instead of `/explore` — `/explore` is still one
click away via the nav sidebar. Update/remove that redirect if the
account name or desired landing target ever changes.
**Changing `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` also requires updating the
Zitadel OIDC app's redirect URI**, or sign-in breaks with
`invalid_request: redirect_uri is missing in the client configuration`.
Forgejo's OIDC callback is always `<ROOT_URL>/user/oauth2/zitadel/callback`
— this is Forgejo's own direct Zitadel app (project **Homelab**, app
**Forgejo**), separate from the shared oauth2-proxy app used by
koffan/it-tools. Update it via the Zitadel console or, from the VPS:
```bash
PAT=$(cat ~/netbird-vps/zitadel/zitadel-data/admin.pat)
curl -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-X PUT "https://zitadel.brianpooe.com/management/v1/projects/345934587441971203/apps/382370882418376707/oidc_config" -d '{
"redirectUris": ["https://git.brianpooe.com/user/oauth2/zitadel/callback"],
"postLogoutRedirectUris": ["https://git.brianpooe.com"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
"appType": "OIDC_APP_TYPE_WEB",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC",
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER"
}'
```
Public git-over-SSH reaches the LXC via a raw TCP forward on the VPS
(`~/netbird-vps/ssh-forward/`, a `socat` container in `network_mode: host`
listening on the VPS's now-freed port 22) over a NetBird route added
specifically for this (`10.0.10.18/32` on the `homelab-lan` network, routed
through the `pi-caddy` peer — see the VPS's `netbird-acl-playbook.md`
"Model 2" pattern). The VPS's own admin sshd was relocated to port 2200 to
free port 22 for this forward.
## Notes
- Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than
floating — Forgejo releases can carry DB migrations, so an unpinned tag
risks pulling a breaking bump on a routine `docker compose pull`.
- `FORGEJO__security__INSTALL_LOCK=true` is baked into the template since all
DB config is supplied via environment — this skips the web installer so a
stray first visitor can't reconfigure the instance.
## Useful Commands
```bash
# Tail logs
docker compose -f docker-compose.forgejo.yml logs -f forgejo
# Create an additional user without open registration
docker exec -it forgejo forgejo admin user create --username <name> --email <email> --random-password
# Recreate after an image update
docker compose -f docker-compose.forgejo.yml pull
docker compose -f docker-compose.forgejo.yml up -d
```