Skip to content

build(backend): add multi-stage Dockerfile + health HTTP-status coupling (Closes #6)#84

Merged
snowrugar-beep merged 1 commit into
VertexChainLabs:mainfrom
gbengaeben:infra/backend-multistage-dockerfile
Jun 23, 2026
Merged

build(backend): add multi-stage Dockerfile + health HTTP-status coupling (Closes #6)#84
snowrugar-beep merged 1 commit into
VertexChainLabs:mainfrom
gbengaeben:infra/backend-multistage-dockerfile

Conversation

@gbengaeben

Copy link
Copy Markdown
Contributor

Summary

Closes #6 (the CI pipeline references docker/backend.Dockerfile that did not exist) and follows up on closed #78 by giving the compression-skip /health endpoint a coupled HTTP-status / body.status contract that liveness probes can actually observe.

Files

New (3):

  • docker/backend.Dockerfile — 6-stage build on node:20-alpine. USER node (UID 1000), HEALTHCHECK via wget --spider against /health, --enable-source-maps so production stack traces map back to TypeScript. apk add drops system openssl (Node 20 ships bundled) and pre-chowns the WORKDIR so the runtime node user can write cwd. The test stage relies on the existing CI gate inside Backend/src/gists/gist.repository.spec.ts:12.
  • Backend/.dockerignore — matches the CI build context (./Backend).
  • docker/README.md — target table, design decisions, explicit headline that the build context must be ./Backend (root-context acceptance commands from the issue cannot satisfy COPY package.json).

Modified (5):

  • infrastructure/ci/docker-build-pipeline.ymlfile: paths corrected to docker/backend.Dockerfile. docker/build-push-action resolves file: relative to the workspace root, not the build context, so no .. traversal is needed.
  • Backend/src/health/health.controller.ts — HTTP status now couples to body.status (200 ok / 503 degraded) via @Res({ passthrough: true }) so Docker HEALTHCHECK and Kubernetes liveness probes flip to failing when a backing service errors out instead of silently reporting green.
  • Backend/src/health/health.controller.spec.ts (new) — 6 Jest unit tests covering the contract.
  • Backend/test/app.e2e-spec.ts and Backend/test/gists.e2e.spec.ts — tolerate 200/503 in the unit sandbox (no live Postgres provisioned) and assert envelope invariants in both branches.

Acceptance criteria mapping (from issue #6)

Criterion Implementation
docker build --target build -f docker/backend.Dockerfile <context> succeeds build stage inherits deps and runs nest build/usr/src/app/dist
docker build --target test -f docker/backend.Dockerfile <context> succeeds and runs tests test stage sets ENV CI=true which activates the existing describe.skip gate at Backend/src/gists/gist.repository.spec.ts:12; runs npm test -- --coverage --testPathIgnorePatterns='\.e2e-spec\.ts$'
docker build --target production -f docker/backend.Dockerfile <context> produces <200 MB image production stage on node:20-alpine + --omit=dev node_modules + compiled dist; no source maps, no dev tooling, no .git
Production container starts and responds on port 3000 ENV PORT=3000; CMD ["node", "--enable-source-maps", "dist/main"]; NestJS listens on process.env.PORT ?? 3000
Production container runs as non-root user USER node (UID 1000, ships with node:20-alpine)
CI docker-build-pipeline.yml passes with the new Dockerfile file: paths fixed; build/test/security-scan/push jobs all point at docker/backend.Dockerfile
Node.js 20 base image ARG NODE_VERSION=20, FROM node:${NODE_VERSION}-alpine AS base
Alpine or distroless in production Alpine (Alpine ships a shell and wget, which lets us use the standard HEALTHCHECK directive without vouching for a Node-based liveness script)
HEALTHCHECK instruction HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD wget --quiet --tries=1 --spider http://127.0.0.1:${PORT}/health || exit 1

Deviations from the literal acceptance commands

The issue uses docker build ... . (repo-root context). That invocation would not work with this Dockerfile because COPY package.json package-lock.json* ./ resolves against the context root — package.json lives at Backend/package.json, not at the repo root. The Dockerfile is therefore built with ./Backend as the context to match how the CI pipeline posts it (context: ./Backend in docker/build-push-action@v5). docker/README.md makes this constraint explicit and lists every verification command with the correct context.

Compression-middleware compatibility (refs #78)

Backend/src/common/middleware/compression.middleware.ts already lists /health in DEFAULT_SKIP_PATHS and skips HEAD/OPTIONS requests. Now that /health returns HTTP 503 when degraded, the new contract is observable by every consumer (Docker HEALTHCHECK, k8s liveness probe, ALB target group) without an additional debug endpoint. Body shape is preserved exactly so existing JSON scrapers parse unchanged.

Verification

  • npx tsc --noEmit — clean
  • npx eslint "{src,test}/**/*.ts" — clean
  • npx prettier --check — clean
  • npx jest src/health — 6/6 pass
  • husky pre-commit (lint-staged) — passes (this commit went through it)
  • Manual local verification commands are listed in docker/README.md.

Risk

Low. Isolated to a new infra asset (CI-time builds + k8s image). No application code paths change; the only change to a controller is the HTTP-status coupling for /health, which behaves identically to before when both PostGIS and Postgres probes pass.

Out of scope / follow-ups

  • Verify the production image stays <200 MB after the first CI build (a useful operational check to file as a follow-up).
  • Two cosmetic follow-ups the reviewer flagged but did not block merge:
    1. NestJS still serves /api/docs at runtime in production (SwaggerModule.setup is unconditional).
    2. The build-stage COPY tsconfig.json is potentially unused if nest build only honors tsconfig.build.json via nest-cli.json.

Refs #6, #78

Closes VertexChainLabs#6 (the CI pipeline references a docker/backend.Dockerfile that
did not exist) and follows up on VertexChainLabs#78 (closed) by honoring the new
/health compression-skip path with a coupled HTTP-status / body.status
contract that liveness probes can actually observe.

New:
- docker/backend.Dockerfile: 6-stage build (base, deps, prod-deps, build,
  test, production) on node:20-alpine. USER node (UID 1000), HEALTHCHECK
  via wget --spider against /health, --enable-source-maps so production
  stack traces map back to TypeScript, no system openssl (Node 20 ships
  bundled), WORKDIR pre-owned so the runtime node user can write cwd.
  Test stage relies on the existing CI gate inside
  Backend/src/gists/gist.repository.spec.ts (verified the only
  Backend/src/**/*spec.ts that touches live DataSource/pg/Pool/TypeORM).
- Backend/.dockerignore: matches the CI build context (./Backend).
- docker/README.md: target table, design decisions, explicit headline that
  the build context must be `./Backend` (root-context acceptance commands
  from the issue cannot copy package.json correctly).

Modified:
- infrastructure/ci/docker-build-pipeline.yml: corrected `file:` paths to
  `docker/backend.Dockerfile`; `docker/build-push-action` resolves `file:`
  relative to the workspace root, not the build context.
- Backend/src/health/health.controller.ts: HTTP status now couples to
  body.status (200 ok / 503 degraded) via @res({ passthrough: true }) so
  Docker HEALTHCHECK and Kubernetes liveness probes flip to failing when
  a backing service errors out instead of silently reporting green.
- Backend/test/app.e2e-spec.ts, Backend/test/gists.e2e.spec.ts: tolerate
  either 200 or 503 in the unit sandbox (no live Postgres provisioned) and
  assert the body envelope invariants in both branches.

Tests:
- Backend/src/health/health.controller.spec.ts: 6 new Jest unit tests
  covering the HTTP-status / body.status contract (success, DB failure,
  PostGIS failure, envelope hygiene).
- tsc --noEmit: clean.
- eslint on src/{,**}/**/*.ts: clean.
- prettier --check: clean.

Refs: VertexChainLabs#6, VertexChainLabs#78
@gbengaeben gbengaeben changed the title infra(backend): add multi-stage Dockerfile + health HTTP-status coupling (Closes #6) build(backend): add multi-stage Dockerfile + health HTTP-status coupling (Closes #6) Jun 23, 2026
@gbengaeben gbengaeben marked this pull request as ready for review June 23, 2026 12:08

Copy link
Copy Markdown
Contributor

Great work @gbengaeben! 🎉 Multi-stage Dockerfile, /health HTTP-status coupling, and the new docker/README.md are exactly what #6 needed. CI is happy across every check, merging this in now.

@snowrugar-beep snowrugar-beep merged commit ae22a36 into VertexChainLabs:main Jun 23, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

infra: create backend Dockerfile with multi-stage build targets

2 participants