33This directory holds container build assets that are checked into source
44control but live outside any single workspace. The CI pipeline
55(` infrastructure/ci/docker-build-pipeline.yml ` ) consumes these files to
6- build, test, scan, and push the NestJS backend image.
6+ build, test, scan, and push the NestJS backend image, and the parallel
7+ ` infrastructure/ci/frontend-build.yml ` workflow consumes the frontend
8+ Dockerfile to build, scan, and push the Next.js standalone image.
79
810## Assets
911
1012| File | Purpose |
1113| -------------------------- | ------------------------------------------------------------ |
1214| ` backend.Dockerfile ` | Multi-stage build for the NestJS server |
15+ | ` frontend.Dockerfile ` | Multi-stage build for the Next.js app (standalone output) |
1316
14- The Backend workspace has its own ` .dockerignore ` because the CI builds
15- with ` context: ./Backend ` . See [ Backend/.dockerignore] ( ../Backend/.dockerignore ) .
16- There is no ` docker/.dockerignore ` : the Dockerfile would mis-copy package.json
17- from a repo-root context, so root-context builds are out of scope.
17+ The ` Backend ` and ` Frontend ` workspaces each have their own ` .dockerignore `
18+ because the CI builds with ` context: ./<workspace> ` . See
19+ [ Backend/.dockerignore] ( ../Backend/.dockerignore ) and
20+ [ Frontend/.dockerignore] ( ../Frontend/.dockerignore ) .
21+ There is no ` docker/.dockerignore ` : a Dockerfile would mis-copy
22+ ` package.json ` from a repo-root context, so root-context builds are out
23+ of scope for both images.
1824
1925## Targets exposed by ` backend.Dockerfile `
2026
@@ -27,7 +33,18 @@ from a repo-root context, so root-context builds are out of scope.
2733` build ` and ` test ` are throwaway CI artefacts; only ` production ` is published
2834to GHCR (` ghcr.io/vertexchainlabs/vertexchain ` ).
2935
30- ## Design decisions
36+ ## Targets exposed by ` frontend.Dockerfile `
37+
38+ | Target | Base image | Purpose | Size envelope |
39+ | --------- | ---------------- | ---------------------------------------------------------------------- | ------------- |
40+ | ` deps ` | ` node:20-alpine ` | ` npm ci ` install (dev + prod deps) for the build runner | ≈ 600 MB |
41+ | ` builder ` | ` node:20-alpine ` | ` next build ` with ` output: 'standalone' ` | ≈ 800 MB |
42+ | ` runner ` | ` node:20-alpine ` | Runtime image: standalone output (traced ` node_modules ` + ` server.js ` ), non-root, healthcheck against ` /api/health ` | < 150 MB (target < 100 MB) |
43+
44+ ` deps ` and ` builder ` are throwaway CI artefacts; only ` runner ` is published
45+ to GHCR (` ghcr.io/vertexchainlabs/vertexchain-frontend ` ).
46+
47+ ## Backend design decisions
3148
32491 . ** Alpine over distroless.** Alpine ships a shell and ` wget ` , which lets
3350 us use the standard ` HEALTHCHECK ` directive without authoring or vetting
@@ -72,14 +89,66 @@ to GHCR (`ghcr.io/vertexchainlabs/vertexchain`).
7289 ` --testPathIgnorePatterns='\.e2e-spec\.ts$' ` . ` node_modules ` is already
7390 excluded by Jest by default, so we list only the e2e pattern.
7491
92+ ## Frontend design decisions
93+
94+ 1 . ** Next.js standalone output.** ` Frontend/next.config.ts ` sets
95+ ` output: 'standalone' ` , which causes ` next build ` to emit a
96+ self-contained ` .next/standalone/ ` containing ` server.js ` , a
97+ pruned ` node_modules ` (only modules traced as required at runtime),
98+ and ` .next/server/ ` . Combined with
99+ ` productionBrowserSourceMaps: false ` , that keeps the runtime image
100+ close to the 100 MB acceptance target without manually curating the
101+ shipped ` node_modules ` . ` .next/static ` and ` public/ ` are still copied
102+ in separately because the standalone output deliberately omits them.
103+
104+ 2 . ** Alpine + ` libc6-compat ` .** Same rationale as the backend image:
105+ Alpine gives the smallest Node base, but Next.js / sharp native
106+ shims link against glibc on some platforms. ` libc6-compat ` is the
107+ standard musl shim that bridges them without giving up the
108+ ≈ 50 MB base size.
109+
110+ 3 . ** No ` prod-deps ` stage (unlike backend).** Where the backend needs
111+ an explicit ` prod-deps ` stage because ` npm prune --omit=dev ` would
112+ otherwise need to run after the fact, Next.js standalone already
113+ traces a production-only ` node_modules ` into
114+ ` .next/standalone/node_modules/ ` during ` next build ` . A second
115+ ` npm ci --omit=dev ` would just duplicate work, so the runner stage
116+ copies the traced tree directly.
117+
118+ 4 . ** Healthcheck via ` /api/health ` .** ` Frontend/src/app/api/health/route.ts `
119+ defines an App Router ` GET ` handler that returns a tiny
120+ ` { status: 'ok', timestamp } ` JSON envelope with
121+ ` dynamic = 'force-dynamic' ` so the route is never pre-rendered into
122+ the static output (which would break the runtime probe on the
123+ standalone server). ` wget --spider ` performs a HEAD-style probe
124+ against ` http://127.0.0.1:${PORT}/api/health ` exactly like the
125+ backend image's ` /health ` probe.
126+
127+ 5 . ** Layer ordering for cache reuse.** ` package.json ` +
128+ ` package-lock.json ` are copied and ` npm ci ` runs * before* any
129+ application source (` next.config.ts ` , ` src/ ` , ` public/ ` ) is copied,
130+ so iterating on TypeScript does not invalidate the ` node_modules `
131+ cache layer. Config files (` next.config.ts ` , ` tsconfig.json ` ) are
132+ copied separately so they live in their own cache layer and can be
133+ invalidated independently of application source.
134+
135+ 6 . ** ` npm ci --ignore-scripts ` .** Skips postinstall hooks
136+ (` husky prepare ` , …) inside the ` deps ` stage. None of those hooks
137+ are required for ` next build ` to succeed, and skipping them avoids
138+ installing build-time-only tools (e.g. native binaries that
139+ postinstall scripts copy into ` node_modules/.bin/ ` ) into a layer
140+ the runner image doesn't actually use.
141+
75142## Local validation
76143
144+ ### Backend
145+
77146``` bash
78- # Build each target standalone. The build context MUST be ./Backend
79- # because `backend.Dockerfile` does relative `COPY package.json ...` and
80- # `COPY src ./src` — these resolve to Backend/package.json and Backend/src
81- # only when the context is Backend/, matching how the CI pipeline posts
82- # `context: ./Backend` to docker/build-push-action.
147+ # Build context MUST be ./Backend because `backend.Dockerfile` does
148+ # relative `COPY package.json ...` and `COPY src ./src` — these resolve
149+ # to Backend/package.json and Backend/src only when the context is
150+ # Backend/, matching how the CI pipeline posts `context: ./Backend` to
151+ # docker/build-push-action.
83152#
84153# Issue #6 example commands use repo-root context (`docker build ... .`).
85154# Those literal invocations are NOT viable with this Dockerfile because
@@ -103,12 +172,55 @@ curl -fsS http://localhost:3000/health
103172docker inspect --format=' {{json .State.Health.Status}}' vertex-backend
104173```
105174
175+ ### Frontend
176+
177+ ``` bash
178+ # Build context MUST be ./Frontend for the same reason as the backend
179+ # image: `frontend.Dockerfile` does relative `COPY package.json`,
180+ # `COPY src ./src`, and `COPY public ./public`, which only resolve
181+ # correctly when the context is Frontend/.
182+
183+ # Install layer only (useful for debugging `npm ci` failures):
184+ docker build --target deps -f docker/frontend.Dockerfile ./Frontend
185+
186+ # Standalone compilation only (useful for tracing `next build` issues):
187+ docker build --target builder -f docker/frontend.Dockerfile ./Frontend
188+
189+ # Ship-shaped runtime image (target < 100 MB per issue #7):
190+ docker build --target runner -f docker/frontend.Dockerfile ./Frontend
191+
192+ # Boot the runtime image and confirm the healthcheck passes:
193+ docker run --rm -p 3000:3000 --name vertex-frontend \
194+ $( docker build -q --target runner -f docker/frontend.Dockerfile ./Frontend)
195+ sleep 10
196+ curl -fsS http://localhost:3000/api/health # liveness probe
197+ curl -fsS http://localhost:3000/ # landing page renders
198+ docker inspect --format=' {{json .State.Health.Status}}' vertex-frontend
199+ ```
200+
106201## Security considerations
107202
203+ ### Backend
204+
108205- Non-root runtime user (` USER node ` ).
109206- Production stage installs only ` --omit=dev ` dependencies and excludes
110207 source maps, dev configs, and ` .env ` files via ` .dockerignore ` .
111208- Image is scanned by Trivy in CI (` infrastructure/ci/docker-build-pipeline.yml ` ,
112209 ` security-scan ` job). High or critical CVEs gate the ` push ` job.
113- - ` TOKEN= ` style secret values are never baked into layers: they must be
114- provided as runtime env vars (` docker run -e KEY=value ` or k8s ` Secret ` ).
210+ - ` TOKEN= ` style secret values are never baked into layers: they must
211+ be provided as runtime env vars (` docker run -e KEY=value ` or k8s ` Secret ` ).
212+
213+ ### Frontend
214+
215+ - Non-root runtime user (` USER node ` , UID 1000).
216+ - Production stage copies only ` .next/standalone/ ` , ` .next/static/ ` , and
217+ ` public/ ` from ` builder ` . Dev tooling (eslint, vitest, typescript,
218+ husky, …) never enters the runtime image.
219+ - Source maps are not inlined into client bundles
220+ (` productionBrowserSourceMaps: false ` in ` Frontend/next.config.ts ` ),
221+ so an attacker pulling the image cannot reconstruct the original
222+ source from client-side bundles.
223+ - Image is scanned by Trivy in CI; high or critical CVEs gate the push.
224+ - ` NEXT_PUBLIC_* ` style values are intentionally * baked in* — that is
225+ the framework contract for browser-visible env vars. Any secret that
226+ must remain server-only belongs on the backend image, not here.
0 commit comments