Phase 4 slice 1: backend release image on the v0.3 line (single worker, non-root) + the phase-4 gate ported onto it #32
Reference in New Issue
Block a user
Delete Branch "po/phase4-backend-image"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What — the backend release image for the v0.3 line, plus the phase-4 gate ported onto the branch the phase actually ships on.
backend/Dockerfile— multi-stage likefrontend/Dockerfile: dependencies from the committed lock (uv sync --frozen --no-dev --no-editable), runtime on python:3.12-slim,uvicorn researchx.api.app:app --host 0.0.0.0 --port 8000 --workers 1as the non-root userresearchx, data dir/var/lib/researchx(RESEARCHX_DATA_DIR), and aHEALTHCHECKon/healthz.backend/.dockerignore— keeps.env*, caches and the local data dir out of the build context..gitea/workflows/ci.yml— the phase-4 gate frommain(researchx#24) copied here, with itspull_requesttrigger extended tomilestone/v0.3-hosted.Why — REQ-83 of the owner-directed roadmap (milestone v0.3, phase 4, status pending): "Backend container: Dockerfile for the FastAPI backend (single uvicorn worker, non-root), two-service deployment (frontend + backend) via deploy.sh extension; RESEARCHX_DATA_DIR volume (default /var/lib/researchx); unauthenticated /healthz outside /api for proxy health checks". Tracked on coreci/researchx#30. This pull request is the Dockerfile half of that requirement; the two-service deploy script is the next slice.
The correction it carries — the first cut of this work (coreci/researchx#31) was based on
main, which is not where phase 4 ships: every v0.3 phase so far landed onmilestone/v0.3-hosted(PRs #16..#19) and tags v0.3.0..v0.3.3 are cut from it.mainand the milestone branch have diverged — 14 commits only onmain, 8 only on the branch — andmainstill carries the v0.2-era app. #31 is closed with that evidence; this one replaces it, based onmilestone/v0.3-hosted@fd3d6c2e03(tag v0.3.3).Reading the app on that branch also corrects two notes in #31:
/healthzdoes exist (a plain GET outside/api, never auth-wrapped — D-103), and the mutable data root does come fromRESEARCHX_DATA_DIR(researchx.config.paths.data_root()— D-107). The image below is written against those contracts, so the healthcheck and the volume mount are real rather than aspirational.How to review — three files, in this order:
backend/Dockerfile— the deps stage reads the committed lock, not the index; the runtime stage drops the build toolchain and the dev dependencies and runs as a non-root user;RESEARCHX_DATA_DIRis the variable the app actually reads; the healthcheck hits/healthz.backend/.dockerignore— secrets and caches stay out of the image..gitea/workflows/ci.yml— the same gate that ran green onmain, plus one added trigger line.Done looks like: the deploy host builds this image from the
backend/context the waydeploy.shbuilds the frontend, and the container answers on port 8000 as userresearchx, with/healthzreturning{"status":"ok"}.Risk — low for the product, two things a reviewer should know:
.gitea/directory at all, which is why the gate is ported here. The first run on this branch is the first gating evidence this code has ever had, and its result is reported on coreci/researchx#30 rather than claimed here.Evidence — branch
po/phase4-backend-imageoffmilestone/v0.3-hosted@fd3d6c2e03(tag v0.3.3); the gate that ran green onmainis run 58204 (backend job 60493, frontend job 60494, both success).TL;DR — the branch is green on all three gate runs: push 69643, push 69644, pull_request 69645, each with both jobs green (backend 71953/71955/71957, frontend 71954/71956/71958). That is the first gating evidence the v0.3 line has ever had — the branch carried no
.gitea/directory before this port.Action — @coreci-hap, @coreci-cpo: review and merge; the merge is the approval (v1.3). Merge this before PR #33, whose two-service deploy script builds the image named here.
Not claimed — no image was built: the gate does not build images and this seat has no Docker. The first build rides the deploy host.
TL;DR — the gate evidence in my earlier comment on this PR now stands confirmed at the current stamp: all three runs concluded success with both jobs green. One new fact for the merge order: the gate file rides in this PR, so PR #33's branch has no gate at all and will need a rebase onto the merged base before it is gated.
What — measured at this seat (own PAT, 04:0xZ):
1eebe94beb) — backend job 71953 success, frontend job 71954 success.1eebe94beb) — backend job 71955 success, frontend job 71956 success.1eebe94beb) — backend job 71957 success, frontend job 71958 success. This is the run I reported as in flight last cycle; it is now concluded green, so the claim on this PR is confirmed, not corrected.Why — the new fact matters for the merge order I asked for. The gate file
.gitea/workflows/ci.ymlexists only on this PR's branch: onmilestone/v0.3-hostedand onpo/phase4-deploy-two-servicethe path.gitea/returns 404 — the milestone branch has never carried a workflow. So PR #33 has zero CI runs: no run record exists for either of its head commits (8afbf1cd,593be870), and its branch cannot produce one, because there is no workflow file on it to run.Action — @coreci-hap, @coreci-cpo: review and merge this PR; the merge is the approval (v1.3). Then, before merging PR #33: rebase it onto the merged
milestone/v0.3-hostedso the gate file it inherits fires. Merging #33 as it stands would put an ungated two-service deploy script into the release path — the one file in this phase that can take the hosted instance down, and the one thing no run has ever exercised.Details — runs 69643/69644/69645 with jobs 71953-71958 read through the actions API at 04:0xZ;
.gitea/workflows/ci.ymlpresence checked per branch by the contents API (200 onpo/phase4-backend-image, 404 onmilestone/v0.3-hostedandpo/phase4-deploy-two-service). The gate does not build images, so a green gate says nothing about the backend image — that leg still rides the deploy host (D-019). PR #33 carries a reviewed fix (comment 16014) that is read-verified and shell-syntax-checked only, not executed: this seat has no Docker.TL;DR — review of this image found one ship-blocking defect in it, and it is fixed on this branch (commit
b964c60e, new head): the container'sCMDran the module-globalresearchx.api.app:app, which is the test factory — it servesFakeModelAdapterand cwd-based storage. Hosting it would have shipped a fake backend behind a public URL that answers/healthz200. The command now runs the production entrypointresearchx.api.main:app.What — read at source, not inferred:
backend/src/researchx/api/app.pyends withapp = create_app()— no arguments — and carries the comment "this serves FAKES with default storage — the production entrypoint (researchx.api.main, Phase 2) must never import this global".create_app()with nomodel_factoryand nomodel_adapterconstructs aFakeModelAdapterwith two canned responses (a fake planner and a fake writer), andstorage_basefalls back toPath.cwd(). The production wiring is a different module:researchx/api/main.pybuilds the app with the liveModelAdapterFactory, the env-file secrets provider, the key store,auth_enabled=Trueandcookie_secure=Truehardwired — and its own docstring saysuvicorn researchx.api.main:appis "the ONLY serving entry the container ever runs (T-4.1)". The repo's own plan agrees:PLAN.mdT-4.1 andRESEARCH-v03.md§8 both specifyCMD ["uvicorn", "researchx.api.main:app", ..., "--proxy-headers"], andRESEARCH-v03.md§8 says it in as many words: "todayapp.py's module-levelapp = create_app()serves FakeModelAdapter + cwd-based storage — hosting that would silently ship fakes."HEALTHCHECKhits/healthz, which is a plain public GET outside/api(app.py:371-373) and answers 200 on either entrypoint. A fake backend and a real one are indistinguishable through the health surface — the difference only shows up in a run's actual model output, on the public URL, in front of a user.b964c60e, one file,backend/Dockerfile): theCMDis now["uvicorn", "researchx.api.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--proxy-headers"].--proxy-headersis what the plan asks for and what the TLS front needs — the host proxy terminates TLS and forwardsX-Forwarded-*, so without it the app sees the proxy's connection, not the browser's.addgroup --gid 10001/adduser --uid 10001 --gid 10001), matchingPLAN.md's "non-root uid 10001" must-have andARCHITECTURE.md's topology. The deploy script's volume chown reads the ownership back out of the built image, so this pins the number instead of letting it drift with the base image's uid allocation.22a451f0fe0b, 3168 B, sha256e0629405cf1febbd).Action — @coreci-hap, @coreci-cpo: review and merge; the merge is the approval (v1.3). The gate has fired on this head — a push run (70266) and a pull_request run (70267) are dispatched on
b964c60e7; neither has concluded, so no green is claimed here. Merge order still applies: #32 before #33, and #33 needs a rebase onto the merged base afterwards, because the gate file rides in this PR (comment 16036).Details —
backend/src/researchx/api/app.pyandbackend/src/researchx/api/main.pyread at the v0.3 tip (milestone/v0.3-hosted@fd3d6c2e03);backend/Dockerfileread on this branch at its previous head1eebe94beb;PLAN.mdT-4.1/T-4.2/T-4.4,RESEARCH-v03.md§8 andARCHITECTURE.md"Deployment Topology" read at the same tip. The one leg this seat cannot close: a real inference call on the hosted instance rides the deploy host and the owner's directive (D-019) — the "live-mode default proven" criterion inPLAN.mdT-4.7 is exactly that leg, and it is not provable from here.