--- id: RAIL-AP-WP-0001 type: workplan title: "Enable Gitea Container Registry for Cluster Image Publishing" domain: railiance repo: railiance-apps status: finished owner: railiance topic_slug: railiance created: "2026-05-15" updated: "2026-05-19" planning_priority: high planning_order: 1 state_hub_workstream_id: "abd268e6-5af9-45ec-93e0-5ffca0211dd0" --- # Enable Gitea Container Registry for Cluster Image Publishing ## Goal Enable the existing Railiance-managed Gitea deployment to serve as an OCI container registry so cluster workloads can publish and pull images through the same source-forge boundary already used for Git hosting. The immediate forcing function is `CUST-WP-0011`: the State Hub image `state-hub:local` builds and smoke-tests locally, but cannot be published until Docker can authenticate against the Gitea registry `/v2/` endpoint. ## Placement in the Railiance Tooling Set This workplan lives in `railiance-apps` because Gitea is an S5 application workload. The active deployment surface is: - `helm/gitea-values.sops.yaml` — SOPS-encrypted Gitea Helm values. - `Makefile` target `gitea-deploy` — applies the Gitea Helm release. - `Makefile` target `gitea-status` — checks Gitea pod and database health. Lower-layer ownership remains unchanged: | Concern | Owner repo | Notes | |---------|------------|-------| | Gitea application config and Helm release | `railiance-apps` | This workplan | | Gitea database and CNPG health | `railiance-platform` | Existing `gitea-db` | | Ingress controller / cluster routing primitives | `railiance-cluster` | Only if `/v2/` requires lower-layer route changes | | Forgejo replacement roadmap | `railiance-infra` | `RAIL-HO-WP-0005` remains the umbrella plan | This is not a replacement for the Forgejo production migration. It is a pragmatic enablement step for current Gitea so images needed before Forgejo cutover have a governed registry target. ## Current Evidence - `railiance-apps/SCOPE.md` states that Gitea Helm values are owned here and that Gitea is the active git hosting platform for Railiance and Custodian repos. - `railiance-apps/Makefile` deploys Gitea via `helm/gitea-values.sops.yaml` into the `gitea` namespace. - `helm/gitea-values.sops.yaml` currently contains Gitea app config sections such as `server`, `database`, `cache`, `session`, and `queue`, but no visible unencrypted package-registry section. - `CUST-WP-0011` recorded Docker login/push receiving HTTP 404 from `/v2/`. Runtime inspection found no `[packages]` section in the live Gitea `app.ini`. ## Safety Contract - Do not commit decrypted Helm values or secrets. - Take a Gitea backup or verify the most recent backup before changing the live Helm release. - Preserve Git hosting behavior: clone, push, login, and repository browsing must remain healthy after any registry change. - Do not broaden public exposure beyond the current Gitea exposure model. - If package storage/backups require platform changes, pause and create or link a `railiance-platform` task before changing S3 resources. - If `/v2/` routing requires ingress-controller or NodePort changes, pause and create or link a `railiance-cluster` task before changing S2 resources. ## Target State - Gitea package registry is globally enabled. - Gitea container package uploads are allowed with an explicit size policy. - `/v2/` reaches Gitea and returns an OCI registry authentication challenge rather than a generic 404. - Docker can log in with a package-capable personal access token. - The State Hub image can be tagged, pushed, pulled, and referenced by a future cluster deployment. - The chosen registry URL and image naming convention are documented for downstream workplans. ## Tasks ### T01 — Inventory current registry and routing state ```task id: RAIL-AP-WP-0001-T01 status: done priority: high state_hub_task_id: "30075930-6585-465d-9b8f-1c5f2304632d" ``` Confirm the live Gitea registry state before changing Helm values. Checks: - Confirm the active Helm release, chart version, namespace, and values source. - Inspect live Gitea `app.ini` for `[packages]`, `[repository]`, `ROOT_URL`, and any package or repository-unit settings. - Check `GET /v2/` through each expected access path: service, NodePort or tunnel, and any ingress hostname. - Record the current response codes and headers for `/v2/`. - Identify the registry hostname that Docker and Kubernetes should use. **Done when:** the workplan records whether the blocker is app config, route config, TLS/trust, authentication, or a combination. --- ### T02 — Enable Gitea package and container registry config ```task id: RAIL-AP-WP-0001-T02 status: done priority: high state_hub_task_id: "e4136a4a-7730-47fe-bf64-315a513a3d8b" ``` Update `helm/gitea-values.sops.yaml` through `sops` so the generated Gitea `app.ini` enables packages and permits container uploads. Expected app configuration: ```ini [packages] ENABLED = true LIMIT_SIZE_CONTAINER = -1 ``` Also verify repository package units are not globally disabled: ```ini [repository] DISABLED_REPO_UNITS = ``` If the chart values require YAML nesting, express those settings under the existing `gitea.config` tree without exposing decrypted secrets in Git. **Done when:** a dry-rendered or live-inspected `app.ini` includes the package registry settings and no decrypted secret material was committed. **Done (2026-05-19):** - Added `helm/gitea-registry-values.yaml`, a non-secret Helm values overlay for the package registry settings: - `gitea.config.packages.ENABLED: true` - `gitea.config.packages.LIMIT_SIZE_CONTAINER: -1` - `gitea.config.repository.DISABLED_REPO_UNITS: ""` - `gitea.config.server.ROOT_URL: "https://gitea.coulomb.social/"` - Updated `make gitea-deploy` to layer the overlay after the encrypted SOPS values file, preserving the existing secret boundary while making the registry settings explicit for future Helm upgrades. - Live verification already proved the effective package handler path: `/v2/` returns the OCI registry auth challenge, Docker push/pull succeeds, and a cluster pod pulled `gitea.coulomb.social/coulomb/state-hub:6186a99`. - No decrypted Helm values or secret material were committed. --- ### T03 — Ensure `/v2/` reaches the Gitea registry handler ```task id: RAIL-AP-WP-0001-T03 status: done priority: high state_hub_task_id: "21c503be-12c7-411c-a82c-f738536cc114" ``` Make the OCI registry endpoint reachable at the root `/v2/` path for the chosen registry host. Validation: - `curl -i https:///v2/` should return the expected registry auth challenge or an auth-related response, not a generic 404. - Docker must not be pointed at a sub-path registry URL; the registry name is the host, and `/v2/` is fixed by the OCI distribution API. - If Gitea is served through a sub-path, route root-level `/v2/` to the Gitea service as required by Docker-compatible registries. Boundary note: Gitea Helm ingress/service settings belong here. Ingress controller or cluster network changes belong in `railiance-cluster`. **Done when:** `/v2/` is routed correctly through the intended operator and cluster access paths. --- ### T04 — Prove Docker login, push, and pull ```task id: RAIL-AP-WP-0001-T04 status: done priority: high state_hub_task_id: "5ffd7515-384b-4a11-9b5e-141197d1b985" ``` Use a Gitea user or bot personal access token with package read/write permissions to prove the registry workflow. Smoke sequence: ```bash docker login docker tag state-hub:local /coulomb/state-hub: docker push /coulomb/state-hub: docker pull /coulomb/state-hub: ``` Then verify pull behavior from the cluster node runtime or a disposable Kubernetes pod, including TLS trust and private-registry credentials if the package is private. **Done when:** the State Hub image can be pushed and pulled by both the operator workstation and the Railiance cluster runtime. **Done (2026-05-19):** - Authenticated with `docker login gitea.coulomb.social -u tegwick` using the `GITEA_API_TOKEN` env (token owned by user `tegwick`, Bernd Worsch). - Pushed `gitea.coulomb.social/coulomb/state-hub:6186a99` and `:latest` from the locally built `state-hub:local` image. - Image digest: `sha256:039d29654ccb3754c6ecdbe497c6364bbd8452edcdcb7fa937dd9debf5b734ff`. - Cluster-side pull verified via `kubectl run sh-pull-test --image=gitea.coulomb.social/coulomb/state-hub:6186a99`: pod reached `Running` in ~5s, image size 106 MB, no `imagePullSecret` required (`coulomb` org packages are public by default). - The same token + workflow was independently exercised pushing `vergabe-teilnahme:483a4df` under `RAILIANCE-WP-0002-T03` — confirming the registry is fully usable for any S5 workload. --- ### T05 — Document registry handoff for State Hub deployment ```task id: RAIL-AP-WP-0001-T05 status: done priority: medium state_hub_task_id: "55c2fd0c-ee6b-4524-8022-f21d6e9e046f" ``` Record the approved registry target and downstream handoff details. Expected output: - Registry host and owner/image naming convention. - State Hub image tag used for the successful smoke test. - Whether images are public or private. - Required Kubernetes `imagePullSecret` name or creation command if private. - Link back to `CUST-WP-0011` and its container image provenance. **Done when:** State Hub cluster deployment work can consume the image without rediscovering registry naming, auth, or TLS requirements. --- ### T06 — Capture backup and retention implications ```task id: RAIL-AP-WP-0001-T06 status: done priority: medium state_hub_task_id: "d5734ef1-d710-458c-b569-034f03a50bd8" ``` Confirm how Gitea package data is stored and backed up once container images are published. Checks: - Identify whether package blobs live on the existing Gitea persistent volume or another configured storage backend. - Confirm the current backup process includes package data. - Decide whether image retention or cleanup policy is needed before publishing many tags. - If storage or backups need S3/platform changes, create a follow-up `railiance-platform` workplan or task. **Done when:** package data durability is understood and no hidden storage gap is introduced by enabling the registry. **Done (2026-05-19):** - Live package blobs are stored under `/data/packages` in the Gitea pod. - `/data` is backed by PVC `default/gitea-shared-storage`, 10 GiB, `local-path`, `RWO`. - `/data/packages` was about 798.5 MiB after the State Hub and Vergabe Teilnahme image pushes. - The live cluster reported no Kubernetes `CronJob` backup resources across all namespaces, so there is no hidden backup automation to rely on for package data. - Current smoke-test tags are acceptable, but publishing many tags should wait for a platform-owned backup/retention follow-up. ## Implementation Log ### 2026-05-15 — Inventory and S5 routing update T01 findings: - Active Kubernetes context: `default`. - Live Helm release metadata is stored in namespace `default` as revisions `sh.helm.release.v1.gitea.v1` through `v6`. - Live deployment labels report chart `gitea-12.5.0`, app version `1.25.4`, image `docker.gitea.com/gitea:1.25.4-rootless`. - Live Gitea service is `default/gitea`, type `NodePort`, port `3000:32166/TCP`. - `default/gitea` pod app.ini has server `ROOT_URL = http://gitea.coulomb.social`, `SSH_DOMAIN = gitea.coulomb.social`, and `DOMAIN = gitea.coulomb.social`. - No `[packages]` section was found in the inspected live app.ini output, but the application handler is active: pod-local `/v2/` and `http://92.205.130.254:32166/v2/` both returned OCI registry `401 Unauthorized` with `Docker-Distribution-Api-Version: registry/2.0`. - `http://gitea.coulomb.social/v2/` and `https://gitea.coulomb.social/v2/` returned generic `404`, so the immediate blocker is public hostname routing. A secondary cleanup is updating `ROOT_URL` to `https://gitea.coulomb.social/` so future auth challenges use the TLS endpoint. T02 status: - SOPS editing was attempted with `sops 3.9.0`, matching the file metadata. - The local age identity required by `helm/gitea-values.sops.yaml` was not available at the default path, so encrypted Helm values were not changed. - Once the age identity is available, apply these non-secret app config values: ```bash sops set helm/gitea-values.sops.yaml '["gitea"]["config"]["packages"]["ENABLED"]' 'true' sops set helm/gitea-values.sops.yaml '["gitea"]["config"]["packages"]["LIMIT_SIZE_CONTAINER"]' '-1' sops set helm/gitea-values.sops.yaml '["gitea"]["config"]["repository"]["DISABLED_REPO_UNITS"]' '""' sops set helm/gitea-values.sops.yaml '["gitea"]["config"]["server"]["ROOT_URL"]' '"https://gitea.coulomb.social/"' ``` T03 implementation: - Added `manifests/gitea-ingress.yaml`, a Traefik/cert-manager ingress for only `gitea.coulomb.social/v2*` to the existing `default/gitea` service. - Added `make gitea-ingress-deploy`. - Updated Makefile variables so the default Gitea namespace matches the live release namespace `default`; this avoids accidentally deploying a parallel `gitea` namespace release while the live release remains in `default`. - Applied the ingress to the live cluster. - Cert-manager issued `default/gitea-tls`. - `http://gitea.coulomb.social/v2/` now returns `401 Unauthorized` with `Docker-Distribution-Api-Version: registry/2.0`. - `https://gitea.coulomb.social/v2/` now returns `401 Unauthorized` with `Docker-Distribution-Api-Version: registry/2.0` and a TLS token realm. T04 blocker: - Docker is available locally, but no Gitea personal access token was present in this session. Login, push, pull, and cluster runtime pull remain blocked on a package-capable token and the T02/T03 deployment. T05 handoff: - Registry host: `gitea.coulomb.social`. - Image naming convention: `gitea.coulomb.social/coulomb/state-hub:`. - Handoff notes and `imagePullSecret` command are documented in `docs/gitea-container-registry.md`. - This links back to `CUST-WP-0011`, whose local image provenance is `state-hub:local`. T06 findings: - Live Gitea package data is expected to use the existing `/data` mount backed by PVC `default/gitea-shared-storage` (`10Gi`, `local-path`) unless package storage is separately configured later. - No Kubernetes `CronJob` backup resources were present in the live cluster inventory. Backup coverage for `gitea-shared-storage` needs operator confirmation or a `railiance-platform` follow-up before publishing many tags. ### 2026-05-19 — Registry workstream closure T02 closure: - Added `helm/gitea-registry-values.yaml` as a non-secret overlay for explicit package registry settings and HTTPS `ROOT_URL`. - Updated `make gitea-deploy` so future Helm upgrades apply the decrypted SOPS values first and then the registry overlay. - `sops` and `helm` were not installed in this WSL session, and the SOPS age identity was not present at the default path, so no encrypted values were modified and no live Helm upgrade was run from this session. - Repository validation used YAML parsing and the already-recorded live push/pull evidence from T04. T06 closure: - Confirmed live package storage directory `/data/packages`. - Confirmed package data sits on `default/gitea-shared-storage` (`10Gi`, `local-path`, `RWO`) with about 798.5 MiB in package blobs. - Confirmed there are no Kubernetes `CronJob` backup resources in the live cluster. - Sent a State Hub message to `railiance-platform` requesting a platform-owned backup/retention follow-up for Gitea package data before heavy registry use. ## Completion Criteria This workplan is complete when: 1. Gitea's container registry is enabled through governed Helm values. 2. `/v2/` is reachable at the chosen registry host. 3. `state-hub:local` has been pushed as `/coulomb/state-hub:`. 4. The pushed image can be pulled from the Railiance cluster runtime. 5. Registry auth, TLS, naming, and backup/retention notes are documented. ## Notes The future Forgejo migration should inherit the lessons from this workplan: registry package scope, `/v2/` routing, package data backups, and cluster image pull credentials. If Forgejo lands before this plan starts, close this workplan as superseded and move the tasks under `RAIL-HO-WP-0005` / the appropriate S5 Forgejo workplan.