Skip to content

Role: forgejo

Self-hosted git server on a dedicated LXC, mirrored to GitHub. Single-user homelab pattern: Forgejo is the source of truth, GitHub is the public-facing mirror.

Host: forgejo — dedicated LXC (CT 109, 192.168.1.249).

Phase 6A complete — 2026-05-04 / 2026-05-05

CT 109 live, Forgejo 11.0.13 (Gitea-API-compat 1.22.0) at https://git.rampancy.cloud, role idempotent. App.ini was patched in-place during 6A.2 (DOMAIN, ROOT_URL, DISABLE_SSH); the role does not template app.ini — wizard generates and operator/runbook patches as needed. All 4 source repos imported and push-mirroring to GitHub.

Why Forgejo, not Gitea / GitLab

  • Governance: hard fork from Gitea (early 2024) under Codeberg e.V. non-profit, vs Gitea Ltd. for-profit. Velocity on Forgejo's side has been higher post-fork.
  • Footprint: ~400 MB idle as a single Go binary. GitLab CE is 4–8 GB just to start.
  • APT-managed: forgejo-contrib/forgejo-deb ships forgejo-sqlite on the official-adjacent contrib repo — drops cleanly into the existing auto_updates flow.
  • Push mirrors are first-class: per-repo config in the UI, no plugin dance.

Architecture

flowchart LR
    subgraph homelab["homelab"]
        subgraph forgejoct["forgejo · CT 109 · 192.168.1.249"]
            F[forgejo-sqlite<br/>:3000<br/>SQLite + repos]
        end
        subgraph edgect["edge · CT 107"]
            C[Caddy reverse proxy]
        end
    end
    subgraph github["github.com"]
        GH[private repo mirrors]
    end
    L[laptop git client] -->|push https| C
    C -->|git.rampancy.cloud → :3000| F
    F -->|push-mirror per repo<br/>fine-grained PAT| GH
    GH -->|Actions on schedule<br/>e.g. meat-helmet| GH

Install method

Forgejo APT repo on code.forgejo.org, signed with the forgejo-contrib keyring. Channel: lts (matches the auto_updates fleet's "security-only" stance better than latest or next).

deb [signed-by=/etc/apt/keyrings/forgejo-apt.asc] \
  https://code.forgejo.org/api/packages/apt/debian lts main

The forgejo-sqlite package brings in:

Path Purpose
/usr/bin/forgejo binary
/etc/forgejo/app.ini config (created by install wizard, not the package)
/var/lib/forgejo/ data, repos, sqlite DB
/etc/systemd/system/forgejo.service unit
forgejo user/group service account

Defaults

Var Default Notes
forgejo_apt_channel lts lts / latest / next
forgejo_package forgejo-sqlite switch to forgejo + postgres only at scale
forgejo_listen_port 3000 LAN-only until 6B fronts it via Caddy
forgejo_external_host git.rampancy.cloud used by 6B Caddy vhost; ROOT_URL set in wizard

Per-host overrides in inventory/host_vars/forgejo.yml — currently just auto_updates_origins_extra to pick up Forgejo lts security updates via the daily timer.

Bootstrap pattern

Web-UI driven on first visit, matching the beszel_hub precedent. The role installs the package and starts the service; the operator visits http://192.168.1.249:3000 and runs the install wizard:

  1. Database type: SQLite (default).
  2. Defaults for paths.
  3. Forgejo Base URL: http://192.168.1.249:3000/ initially; flipped to https://git.rampancy.cloud/ after Phase 6B.
  4. Admin account created via wizard using vault_forgejo_admin_password (vault stores for rebuild documentation; the role doesn't consume it).

Once /etc/forgejo/app.ini exists, subsequent role runs become idempotent (changed=0 under --check --diff).

What this role does NOT do

  • Does not template app.ini. The wizard generates it; later phases may layer config on top.
  • Does not create the admin account programmatically. Web UI handles it.
  • Does not configure push mirrors. Per-repo, set up in Phase 6C.
  • Does not run a Forgejo Actions runner. Deferred — meat-helmet's GH Actions keep firing on the GitHub side after push-mirror.