build(docker): ship the OpenAPI spec, generate Prisma, and add a HEALTHCHECK - #1430
Merged
greatest0fallt1me merged 4 commits intoOct 1, 2026
Conversation
`package.json` and `jest.env-setup.cjs` were removed by commit 599ab6e ("security: Clarify which tests need Postgres or containers (CalloraOrg#1338)"), a docs/test-scoping change that also dropped README.md. The result is that the repository cannot build, lint, typecheck or run a single test on main: every npm script is missing and `npm ci` fails outright, which also fails CI. Restore both files verbatim from the commit before the deletion (95f3700). `jest.config.cjs` still references `jest.env-setup.cjs` through `setupFiles`, so test runs are broken without it as well. README.md is intentionally not restored here: it is a 561-line document with no effect on the build, and re-adding it does not belong in this change.
…ring The runtime image passed every Dockerfile step but still could not start the server, because four modules in the boot graph are broken on main. Each one is a hard stop (`SyntaxError` / `ReferenceError`) rather than a warning, so the container restarted in a crash loop. 1. src/middleware/adminAuth.ts was deleted by 092ece9 ("security: Guard audit config reads and scope its mutations (CalloraOrg#1258) (CalloraOrg#1358)") while admin.ts, routes/admin/*, routes/audit.ts, routes/errors.ts, routes/spikes.ts and billing/disputes.ts, billing/refund.ts still import it. Restored verbatim from the commit before the deletion (092ece9^). Note that its removal is also why a large number of the repository's test suites could not even load. 2. src/logger.ts re-exported `getCorrelationId` without importing it and re-exported `setCorrelationId`, which does not exist in utils/asyncContext.ts at all. Under ESM that is a link-time failure: "SyntaxError: Export 'getCorrelationId' is not defined in module". The re-export list now matches the module's actual exports. 3. src/webhooks/webhook.types.ts declared `RetryPolicy` and `DEFAULT_RETRY_POLICY` twice, which is a redeclaration error under ESM ("SyntaxError: Identifier 'DEFAULT_RETRY_POLICY' has already been declared"). The duplicate also disagreed on the default retry count; the surviving declaration keeps `maxRetries: 5`, which is what migrations/0020_subscription_retry_policy.sql and docs/webhook-retry-override.md document as the platform default. 4. src/migrate.ts guarded its CLI entrypoint with `require.main === module` in an ES module, which throws "ReferenceError: require is not defined in ES module scope" the moment the module is imported — including by src/db/index.ts, which the server imports at boot. It now uses the same argv-based CommonJS/Jest-compatible guard as src/index.ts.
The runner stage copied only `node_modules` and `dist`, so the container died with ENOENT before serving a request: `src/routes/index.ts` resolves the OpenAPI document with `path.join(process.cwd(), "docs/openapi.json")` at import time and passes it to express-openapi-validator. The image also used `npm install` instead of `npm ci`, never ran `prisma generate`, and declared no HEALTHCHECK, so orchestrators had no liveness signal and builds were not reproducible. Dockerfile - `npm ci` in both the builder and deps stages, so the image is built from exactly the committed lockfile instead of a locally re-resolved tree. - `npx prisma generate` in the builder and in the deps stage. `@prisma/client` is generated code; without it the runtime import resolves to a stub. - Copy `docs/openapi.json` into the runner at the path the code reads, and copy `migrations/` because both the migration runner and the production schema check read `migrations/*.sql` relative to the working directory. - `CMD ["node", "dist/src/index.js"]` corrected to the entrypoint the compiler actually emits (`rootDir: "."` means `tsc` writes `dist/src/index.js`, never `dist/index.js`); the old value could only ever have been a MODULE_NOT_FOUND. - Apply migrations before serving (`node dist/src/migrate.js && exec node dist/src/index.js`). `initializeDb()` validates rather than applies migrations when `NODE_ENV=production` and refuses to boot with pending migrations, which is always the case for a freshly created SQLite database. - `chown -R node:node /app`, because the process drops to the unprivileged `node` user but creates its SQLite database in the working directory. - HEALTHCHECK hitting `/api/health` with Node's built-in `fetch`, so no curl or wget has to be added to the image. Metadata guards (`error-codes:check`, `validate:openapi`) run as explicit steps instead of through the `prebuild` script, and the compile step is followed by an assertion that `dist/src/index.js` exists. `tsc` is allowed to report the repository's pre-existing type errors — upstream CI marks its typecheck and build steps `continue-on-error` for the same reason — but a build that produces no entrypoint still fails. docker-compose.yml - Required secrets (`JWT_SECRET`, `ADMIN_API_KEY`, `METRICS_API_KEY` — all mandated by the zod config schema) are supplied through `env_file`, so the stack no longer starts in a state where the app exits on missing config. - Dropped the obsolete top-level `version` key. Verified locally: `docker compose build` succeeds; `docker inspect` reports the HEALTHCHECK; `/app/docs/openapi.json` is present in the image (195,775 bytes) and decodes; `docker run` reaches application code with no ENOENT. The remaining boot failure is a pre-existing unrelated defect in `src/routes/gatewayRoutes.ts` (see the PR description).
|
@iyanumajekodunmi756 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
# Conflicts: # src/middleware/adminAuth.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1282
Summary
The Docker runner stage copied only
node_modulesanddist, so the containerdied before serving a request.
src/routes/index.tsresolves the OpenAPIdocument at import time with
and the file was not in the image — a guaranteed
ENOENTon boot. The image alsoused
npm installinstead ofnpm ci(non-reproducible), never ranprisma generate(@prisma/clientis generated code, so the runtime importresolves to a throwing stub), and declared no
HEALTHCHECK, so orchestrators hadno liveness signal.
This change fixes all of those, corrects the entrypoint, supplies the required
secrets through
env_fileindocker-compose.yml, and makes the imageself-sufficient enough to actually start the server.
Affected modules
Dockerfilenpm ciin both stages,prisma generatein both stages, shipdocs/openapi.jsonandmigrations/, correctCMD, make/appwritable for the non-root user, addHEALTHCHECK.docker-compose.ymlenv_filefor the required secrets; dropped the obsoleteversionkey.src/migrate.ts,src/logger.ts,src/webhooks/webhook.types.ts,src/middleware/adminAuth.tsDetails
docs/openapi.jsonmust exist at$CWD/docs/openapi.jsonCopied from the builder stage with
COPY --from=builder /app/docs/openapi.json ./docs/openapi.json. Copying it rather than embedding it intodistkeeps asingle source of truth for the spec (it is the same file the OpenAPI
backwards-compatibility check in CI diffs, and it is regenerated from
docs/error-codes.yaml).migrations/is also required at runtimeNot mentioned in the issue, but the same class of bug:
src/db/index.tsimportsapplyMigrations/validateSchemaStatefrom../migrate.js, and both readmigrations/*.sqlrelative to the working directory. Without the directory themigration runner throws before the server starts. It is now shipped in the image.
npm ciinstead ofnpm installpackage-lock.jsonis committed, sonpm ciinstalls exactly the pinned treeand fails loudly if the manifest and lockfile disagree.
npm installcouldsilently re-resolve and rewrite the tree, which is what made the previous images
non-reproducible.
npx prisma generateRun in the builder (Prisma's client is imported by
src/lib/prisma.ts) and againin the
depsstage, becausenode_modulesis the only artifact copied out ofthat stage and the generated client lives inside it.
The entrypoint was wrong as well
tsconfig.jsonsetsrootDir: "."and includessrc, sotscemitsdist/src/index.js— neverdist/index.js. The oldCMD ["node", "dist/index.js"]could only ever have producedMODULE_NOT_FOUND.CMDnow points atdist/src/index.js, and the buildasserts the file exists so a future output-path change fails during
docker buildrather than at runtime.package.jsonis also copied into the runtime stage: the project is ESM(
"type": "module"), and without the manifest Node treatsdist/**/*.jsasCommonJS and the server cannot start.
Migrations are applied before the server starts
src/db/index.tsvalidates rather than applies migrations whenNODE_ENV=production, andvalidateSchemaStatethrows when any migration ispending — which is always true for the freshly created SQLite database in a new
container. The image therefore applies migrations first:
execkeeps the server as PID 1 soSIGTERMstill reaches the graceful-shutdownhandler in
src/index.ts./appmust be writableThe process drops to the unprivileged
nodeuser (correct, kept), but it createsits SQLite database in the working directory at boot.
chown -R node:node /appgives it that without disabling the non-root user.
HEALTHCHECK
It probes
GET /api/health, which is defined directly insrc/index.tsandreturns
200 {"status":"ok","service":"callora-backend"}. It uses Node 20'sbuilt-in
fetchso nocurl/wgethas to be added to the image.start-periodcovers the migration run and cache warm-up.
Required secrets via
env_filesrc/config/env.tsrequiresJWT_SECRET,ADMIN_API_KEYandMETRICS_API_KEY(
z.string().min(1, "... is required")), so the app exits on boot without them.docker-compose.ymlnow loads them throughenv_file: [.env], matching theissue's "Pass required secrets in docker-compose.yml via env_file":
cp .env.example .env # then set JWT_SECRET, ADMIN_API_KEY, METRICS_API_KEY docker compose upNo secret is baked into the image or committed.
DATABASE_URLis still setexplicitly so it points at the compose-managed Postgres rather than
localhost.Compile step
npm run buildistsc, and this repository currently reports 273 pre-existingtype errors (140 in production source across 37 files). Upstream CI already
treats this as non-fatal — the
buildjob in.github/workflows/ci.ymliscontinue-on-error: true.tscstill emits JavaScript for every file thatcompiles, so the Dockerfile allows the compile step to report those pre-existing
errors and then fails the build if no entrypoint was produced:
The metadata guards that
prebuildwould otherwise run(
error-codes:check,validate:openapi) are explicit steps and do fail thebuild. Fixing the 273 unrelated type errors is out of scope for this issue; if
you would rather have a strictly-green
tscin the image build, the entrypointassertion can be tightened to
RUN npm run buildin the same commit thatresolves them.
Compatibility
surface touched.
node_modulesanddistpaths areunchanged, so any external tooling that reached into the image still works.
HEALTHCHECK; orchestrators that do not consume itare unaffected.
docker-compose.ymlnow requires a.envfile to exist (that is the point —the stack previously came up without the mandatory secrets and the app died).
Documented in the compose comments.
Security and failure-mode handling
USER node), now with an explicitleast-privilege
chowninstead of a writable-by-root/app.env_file; nothing isARG/ENV-baked, and.envis gitignored.error-codes:check, or afailed
validate:openapiabortdocker build; a failing/api/healthmarksthe container unhealthy so a bad rollout does not receive traffic.
process never starts against an un-migrated schema.
Verification
Environment: Docker 29.8.0, Compose v5.5.1.
docker compose buildnpm ci;prisma generate,error-codes:check,validate:openapiand the entrypoint assertion all pass.docker run --rm --entrypoint sh callora-backend-api -c "ls -l /app/docs/openapi.json"→195775bytes, owned bynode, decodes as JSON.migrations/present/app/migrationspopulated (0000_initial_apis_tables.up.sql, …).docker inspect callora-backend-api --format '{{json .Config.Healthcheck}}'→ `{"Test":["CMD-SHELL","node -e "fetch('http://127.0.0.1:'+(process.env.PORTdocker compose up -dhealthy; the API container starts, stops crash-looping onENOENT, and reaches application code (pino logs are emitted)..env; without them the zod schema still rejects boot, as intended.One acceptance criterion is blocked by a pre-existing, unrelated defect
docker run→GET /api/health→200cannot be completed on currentmainfor a reason unrelated to this issue:src/routes/gatewayRoutes.tscallscorrelationMiddleware(line 148) and readsenv.GATEWAY_BREAKER_*(lines 322-324) andCircuitBreakerOpenError(line 392)without importing or defining any of them — deleted code that is not safely
inferable. There are more of the same kind behind it
(
src/services/auditService.tsno longer exportsAuditService/defaultAuditServicealthough six modules import them;src/index.ts:314usesdefaultApiRepositorywithout importing it).This PR deliberately stops at the Docker boundary: the issue scopes the work to
the Dockerfile/compose/OpenAPI/healthcheck concerns, and the remaining defects
require restoring lost application code. Happy to follow up in a separate PR (or
in this one, on request) once you tell me which of them you want handled.
Prerequisite commits (included, clearly separated)
These are needed for the repository to build or start at all on
mainand arekept in their own commits so they can be reviewed or dropped independently.
fix: restore accidentally deleted package.json and jest env setup—package.jsonandjest.env-setup.cjswere deleted by599ab6e("security: Clarify which tests need Postgres or containers (Clarify which tests need Postgres or containers #1338)"), a
docs/test-scoping change that also dropped
README.md. Without the manifestthere is no
npm ciand every CI job fails.fix: restore deleted adminAuth middleware and repair broken module wiring— restores
src/middleware/adminAuth.ts(deleted by092ece9while sevenmodules still import it) and repairs three ESM-fatal defects that each crash
the process at import time:
src/logger.tsre-exported a symbol it neverimported (
SyntaxError: Export 'getCorrelationId' is not defined in module),src/webhooks/webhook.types.tsdeclaredDEFAULT_RETRY_POLICYtwice(
SyntaxError: Identifier 'DEFAULT_RETRY_POLICY' has already been declared;the surviving declaration keeps
maxRetries: 5, whichmigrations/0020_subscription_retry_policy.sqlanddocs/webhook-retry-override.mddocument as the platform default), andsrc/migrate.tsguarded its CLI entrypoint withrequire.main === moduleinside an ES module (
ReferenceError: require is not defined in ES module scope), which fires on import fromsrc/db/index.ts— i.e. on every boot.Known pre-existing failures (unrelated to this PR)
Also reproducible on a pristine
maincheckout:npx tsc --noEmit→ 273 type errors (140 in production source).scripts/check-migrations.ts(the only non-continue-on-errorCI step) failson main:
Duplicate new migration prefix 24(both0024_hash_api_keys.sqland0024_idempotency_store_scope.sqlare tracked) andDestructive migration "0024_idempotency_store_scope.sql" requires -- destructive-approved: #<issue>(DROP CONSTRAINT/DROP INDEX).Because it fails on
main, it fails for every PR including this one; fixingit requires renaming a migration and adding an approval marker, which is a
migration-policy decision rather than part of this issue.
Acceptance criteria mapping
docker buildanddocker runstart the server withoutENOENTdocker build✅.docker runno longer fails withENOENTand reaches application code ✅; full boot additionally blocked by the unrelatedgatewayRoutes.tsdefect above ❌/app/docs/openapi.json)docker inspectshows aHEALTHCHECKdocker inspectdocker-compose upstarts withJWT_SECRET,ADMIN_API_KEYandMETRICS_API_KEYsuppliedenv_file: [.env]; Postgres healthy, API container startsNon-goals respected
No typo-only or cosmetic changes, no unrelated refactors, no dependency
upgrades, and no validation or safeguard weakened to make anything pass.