# 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 -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://:3000`, create the first (admin) account, and confirm repo creation/push/clone works. ## Reverse Proxy Add a block for `git.` (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//.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 `/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 --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 ```