Forgejo on the NAS
What: deploy Forgejo as a TrueNAS custom app at git.nas.fzymgc.house, reachable on the LAN
and the tailnet, with a Keycloak sign-in and a Kopia backup.
Design: docs/engineering/specs/2026-09-21-nas-forgejo-design.md. The decisions there are
agreed and are not reopened here.
Riskiest assumption: that the rootless image accepts user: 568:568 with cap_drop: [ALL]
and still writes app.ini to its config mount at first start. The upstream example overrides the
user, so this is expected. The first converge proves it.
1. Decisions you’ll probably want to tweak
Section titled “1. Decisions you’ll probably want to tweak”| # | Decision | Alternative | Cost of changing later |
|---|---|---|---|
| P1 | The app runs as uid 568:568, the appliance apps user, as RustFS does |
The image default 1000:1000 |
Low. Chown the dataset and change one default. Uid 568 is chosen so the same ACL walk that already serves RustFS covers this dataset. |
| P2 | Pin 16.0.5-rootless, the newest stable tag in the registry on 2026-09-21 |
Pin the major 16-rootless |
Low. Renovate proposes each patch, which is the visibility the pin exists for. |
| P3 | Forgejo’s built-in SSH server listens on 2222 inside the container | An OpenSSH sidecar | Low. The rootless image has no sshd, and the built-in server needs no host key management. |
| P4 | The Keycloak client ships in a second PR, after the Vault grant applies | One PR for both | None. The grant and the client sit in different HCP workspaces, and a combined PR races them. |
| P5 | Mirrors, Actions and self-registration are off by configuration, not by omission | Leave defaults | Low. Explicit keys make the scope boundary greppable. |
2. Known unknowns
Section titled “2. Known unknowns”| Unknown | Default | Pivot signal |
|---|---|---|
Does pool.dataset.create with generate_key: true return the key, or does it stay in the appliance keystore only? |
The keystore holds it, and the operator exports it once with pool.dataset.export_key |
The role’s create task returns a key field. Then the runbook step changes to “copy it from the converge output”. |
Does Traefik pick the nas-internal address for a container on two networks? |
Yes, with the traefik.docker.network label |
A 502 or a Gateway Timeout from git.nas.fzymgc.house. Then read the address Traefik chose from its dashboard. |
| Does the Kopia sandbox see the new dataset without a container restart? | No. New FILESYSTEM devices mount at container start | The first backup log names /mnt/main/forgejo as missing. Restart nas-support. |
Does the health check in the compose pass under REQUIRE_SIGNIN_VIEW? |
Yes. /api/healthz is exempt from sign-in |
The app reports unhealthy while the UI works. Then drop the health check. |
3. Work
Section titled “3. Work”Phases run in order. Each phase ends in a commit on this branch, except phase 3, which is its own PR, and phase 4, which is an operator session.
Phase 1: repository changes
Section titled “Phase 1: repository changes”tf/vault/policy-keycloak.tf: grantcreate,readandupdateonsecret/data/fzymgc-house/infrastructure/nas/forgejo/oidcand itsmetadatapath.tf/dns-records/hosts.yml: addgit_nasat192.168.20.201and the Traefik ULA,ptr: false.ansible/roles/nas-forgejo/: defaults, meta, tasks, and a compose template. The task file has three regions: dataset and directories, secret files, app reconcile.ansible/nas-playbook.yml: a play onnaswith tagnas-forgejo.ansible/inventory/group_vars/all.yml:main/forgejoinnas_kopia_leaves.ansible/inventory/host_vars/nas.yml:main/forgejoinnas_sandbox_mounts.docs/operations/nas.md: a tag map row and a Forgejo section.docs/reference/services.md: one row.
Phase 2: lint and review
Section titled “Phase 2: lint and review”rumdl check --config docs/.rumdl.tomlon the docs.yamllintandansible-linton the role, through the pre-commit hook.terraform fmt -checkon the two Terraform files.- Push and open the PR. The operator merges.
Phase 3: Keycloak client (second PR)
Section titled “Phase 3: Keycloak client (second PR)”tf/keycloak/forgejo.tf: clientforgejo, standard flow, redirecthttps://git.nas.fzymgc.house/user/oauth2/keycloak/callback, secret to the/oidcpath.- Open after the
main-cluster-vaultrun from phase 2 showsapplied.
Phase 4: operator session
Section titled “Phase 4: operator session”- Seed the Vault secret:
vault kv put secret/fzymgc-house/infrastructure/nas/forgejo secret_key=<64 random chars> internal_token=<jwt>. Forgejo prints both withforgejo generate secret SECRET_KEYandforgejo generate secret INTERNAL_TOKEN, which run from any container of the image. - Converge:
scripts/nas-playbook.sh --tags nas-forgejo. - Export the dataset key to Vault:
midclt call -j pool.dataset.export_key main/forgejo, thenvault kv put secret/fzymgc-house/infrastructure/nas/forgejo-zfs-key key=<hex>. - Create the admin: the
docker execcommand in the runbook section. - Add the Keycloak authentication source at
/admin/auths/newand link the identity. - Restart
nas-support, thenscripts/nas-playbook.sh --tags nas-kopia-acls. - Run the acceptance commands from the spec, on the LAN and on the tailnet.
4. Acceptance
Section titled “4. Acceptance”The spec, section 8, lists the commands. Two additions:
scripts/nas-adhoc.sh nas -b -m command -a 'midclt call app.query [["name","=","forgejo"]]' # state RUNNINGscripts/nas-playbook.sh --tags nas-forgejo --check --diff # changed=0 on the second run