--- id: KEY-WP-0002 type: workplan title: "KeyCape Container Image — Build & Publish to Gitea OCI Registry" domain: netkingdom repo: key-cape status: done owner: netkingdom topic_slug: netkingdom created: "2026-03-22" updated: "2026-03-21" capability_request_id: "" state_hub_workstream_id: "c8843c7a-460a-47a2-b45a-b8d3940f9aa2" --- # KEY-WP-0002 — KeyCape Container Image — Build & Publish to Gitea OCI Registry ## Problem KeyCape has a `Dockerfile` but no automated build pipeline and no published image. Other services (k3s deployments, local dev) that need to run KeyCape must build locally from source. There is no versioned artefact to reference in Helm charts or manifests. The capability request for this work was originally misrouted to railiance. It belongs here: KeyCape owns its own image. ## Goal Produce a versioned OCI image for KeyCape, published to the Gitea container registry on CoulombCore (`92.205.130.254:32166`), triggered automatically on every merge to `main` and on semver tags (`v*`). **Gitea OCI registry endpoint:** `92.205.130.254:32166` **Image name:** `92.205.130.254:32166/coulomb/key-cape` > **Why Gitea, not GHCR?** > The net-kingdom cluster is self-hosted. Keeping images in Gitea (also > self-hosted on CoulombCore) avoids any external registry dependency and > keeps image pulls within the cluster network. GHCR is a future option > once public distribution is needed. ## Design ### Image naming & tagging | Trigger | Tags applied | |---------|-------------| | push to `main` | `latest`, `main-` | | tag `v1.2.3` | `1.2.3`, `1.2`, `1`, `latest` | ### Build Multi-stage Dockerfile already present — no changes needed to the build itself. The image builds to a distroless static binary (~10 MB). ### Registry auth Gitea issues a personal access token (or machine account token) with `write:packages` scope. Stored as Gitea Actions secret `REGISTRY_TOKEN`; username stored as `REGISTRY_USER`. For local `make push`, credentials are passed via `docker login` before the push target runs. ### Makefile targets ```makefile IMAGE_REGISTRY ?= 92.205.130.254:32166 IMAGE_REPO ?= coulomb/key-cape IMAGE_TAG ?= latest IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG) image: docker build -t $(IMAGE) . push: image docker push $(IMAGE) image-tag: docker tag $(IMAGE) $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG) ``` ### Gitea Actions workflow `.gitea/workflows/image.yaml` — triggers on push to `main` and on `v*` tags: - Checkout - Set up Docker Buildx - Login to `92.205.130.254:32166` using secrets - Build and push with metadata-action tags - (Optional) sign with cosign if available ### k3s insecure registry Gitea runs over plain HTTP on port 32166 (NodePort). k3s must be configured to treat this endpoint as an insecure registry so image pulls work from within the cluster: ```yaml # /etc/rancher/k3s/registries.yaml (on CoulombCore) mirrors: "92.205.130.254:32166": endpoint: - "http://92.205.130.254:32166" ``` k3s picks this up on restart (or SIGHUP). Worker nodes (if any) need the same file. --- ## Tasks ### T01 — Makefile: image, push, image-tag targets ```task id: KEY-WP-0002-T01 status: done priority: high state_hub_task_id: "749472fc-edb9-4948-9ebc-58d5f38327ee" ``` Add `image`, `push`, and `image-tag` targets to `Makefile` with `IMAGE_REGISTRY`, `IMAGE_REPO`, `IMAGE_TAG` variables defaulting to the Gitea endpoint and `coulomb/key-cape:latest`. Gate: `make image` builds successfully locally; `IMAGE_TAG=dev make image` produces a differently-tagged image. --- ### T02 — Gitea Actions workflow ```task id: KEY-WP-0002-T02 status: done priority: high state_hub_task_id: "8ecf18cc-a3bb-4ede-a09c-fcd0d26d7f9d" ``` Create `.gitea/workflows/image.yaml`: - Trigger: `push` to `main`, `push` tags matching `v*` - Runner: `act_runner` label (or `ubuntu-latest` if configured) - Steps: checkout → docker buildx → login → build+push - Tags via `docker/metadata-action`: `latest` on main, semver on tags Secrets required (document in README.md under "CI"): - `REGISTRY_USER` — Gitea username or machine account - `REGISTRY_TOKEN` — Gitea personal access token with `write:packages` Gate: workflow file is syntactically valid; documented in README. --- ### T03 — k3s insecure registry config on CoulombCore ```task id: KEY-WP-0002-T03 status: done priority: high state_hub_task_id: "2dde67f9-944f-418d-a2e9-7367bc556425" ``` On CoulombCore, create/update `/etc/rancher/k3s/registries.yaml` to add the Gitea NodePort as an HTTP mirror. Restart k3s (or send SIGHUP) and verify `crictl pull 92.205.130.254:32166/coulomb/key-cape:latest` works. Gate: image pull from within the cluster succeeds without TLS errors. --- ### T04 — Create Gitea machine account & token ```task id: KEY-WP-0002-T04 status: done priority: medium state_hub_task_id: "25775e10-3164-4adb-9c41-835c86fde5f8" ``` In Gitea (http://92.205.130.254:32166), create a machine account `ci-netkingdom` (or reuse an existing service account) with access to the `netkingdom` organisation. Generate a token with `write:packages` scope and store it in: - Gitea Actions secrets on the `key-cape` repo: `REGISTRY_USER`, `REGISTRY_TOKEN` - The net-kingdom credential store (SOPS-encrypted) under `credentials/gitea-ci-token.enc.yaml` Gate: `docker login 92.205.130.254:32166` succeeds with the token; secret is in the credential store. --- ### T05 — Smoke test: push and pull a dev image ```task id: KEY-WP-0002-T05 status: done priority: medium state_hub_task_id: "0f6ab38f-6d34-41af-9180-f19c687947b5" ``` Manually trigger a build-and-push: ```bash docker login 92.205.130.254:32166 IMAGE_TAG=dev make push ``` Then verify the image is pullable from CoulombCore: ```bash # on CoulombCore crictl pull 92.205.130.254:32166/coulomb/key-cape:dev ``` Gate: pull succeeds; image is listed in Gitea -> Packages -> coulomb/key-cape. --- ### T06 — Update README with registry & CI docs ```task id: KEY-WP-0002-T06 status: done priority: low state_hub_task_id: "946cd34d-94da-4fa9-a781-ed36f6c827a3" ``` Add a "Container Image" section to `README.md` documenting: - Registry URL and image name - How to pull (`docker pull 92.205.130.254:32166/coulomb/key-cape:latest`) - How to build and push locally (Makefile targets) - CI secrets required for the Actions workflow Gate: README section present and accurate. --- ## Done Criteria - [ ] `make image` and `make push` work locally - [ ] `.gitea/workflows/image.yaml` present and documented - [ ] k3s can pull the image from Gitea without TLS errors - [ ] Machine account token stored in credential store - [ ] Smoke test: `dev` image pushed and pulled successfully - [ ] README updated