homelab-blueprint/docker-compose-files/forgejo
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
..
.env.sample feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access 2026-07-19 18:34:13 +02:00
README.md feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access 2026-07-19 18:34:13 +02:00
template.yaml fix(forgejo): set SSH_DOMAIN so the UI's clone URL is actually usable 2026-07-19 15:17:25 +02:00

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 — 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:

    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 (add forgejo_data to the share list there, same No Root Squash treatment as paperless_data):

    # 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:

    make deploy-forgejo
    

    Manual equivalent:

    ./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.com10.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:

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

# 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