Skip to content

feat(devops): Docker multi-stage production build optimization & security hardening (#1335) - #1582

Merged
K1NGD4VID merged 6 commits into
LabsCrypt:mainfrom
nemeregiftbrown-byte:devops/docker-multistage-hardening-1335
Oct 8, 2026
Merged

K1NGD4VID merged 6 commits into
LabsCrypt:mainfrom
nemeregiftbrown-byte:devops/docker-multistage-hardening-1335

Conversation

@nemeregiftbrown-byte

Copy link
Copy Markdown
Contributor

Summary

Closes #1335.

Production images for the FlowFi backend and frontend are now multi-stage, run as a non-root user, and expose a native Docker healthcheck — plus the Compose stack builds and runs the frontend.

Backend — backend/Dockerfile

  • Kept the multi-stage builder → runner split on the pinned node:20-alpine digest.
  • Runs as the non-root node user (USER node).
  • Added HEALTHCHECK polling http://127.0.0.1:3001/health.
  • CMD ["node", "dist/index.js"] instead of npm start, so the server is PID 1 and receives SIGTERM/SIGINT for the graceful-shutdown handler in src/index.ts.
  • COPY --chown=node:node (no extra chown -R layer), npm cache clean, dropped the redundant duplicate prisma copy.

Frontend — frontend/Dockerfile (new) + frontend/next.config.ts

  • Enabled Next.js output: "standalone" (the issue's requirement).
  • Multi-stage build whose runner copies only .next/standalone + .next/static + public — never the workspace node_modules.
  • Non-root node user + native HEALTHCHECK on /health.
  • Added a dependency-free frontend/src/app/health/route.ts.
  • NEXT_PUBLIC_* accepted as build args (they are inlined into the client bundle at build time).

Compose & build context — docker-compose.yml, root .dockerignore

  • New frontend service with build context at the repo root so the standalone monorepo layout (frontend/server.js) is preserved; depends_on: backend (healthy).
  • Healthchecks on backend and frontend, with start_period for warm-up.
  • Probes hit 127.0.0.1 deliberately: busybox wget resolves localhost to IPv6 ::1 first, which the IPv4-bound servers do not answer.
  • Root .dockerignore keeps the context lean (.git, node_modules, build output, tests, docs, contracts).

Verification

Frontend image built and run locally:

  • docker build -f frontend/Dockerfile . → success
  • State.Health.Status → healthy; GET /health → 200 {"status":"ok"}; GET / → 200
  • runs as uid=1000(node)
  • size: 282 MB uncompressed / ~67 MB compressed

Backend Dockerfile parses and its dependency/COPY stages run; the final npm run build step fails on a pre-existing prisma/tsc breakage on main (see below), not on the Dockerfile itself.

Pre-existing blockers on main (NOT introduced here)

While verifying I found main is already broken in ways unrelated to #1335:

  1. backend/prisma/schema.prisma declares IndexerDeadLetterEvent twice, and two migrations create the same table (bad merge 38a3e2d). prisma generate therefore fails → the backend image cannot build until this is resolved.
  2. backend/package.json requires @stellar/stellar-sdk@^17.0.1 while the committed lockfile resolves 15.1.0 (commit 4a8009c), and the code was not migrated to the v17 API — so npm ci fails the sync check and tsc reports 76 errors in the backend / 19 in the frontend.
  3. Because of (2), next build currently fails on pre-existing frontend type errors; the frontend image was validated with a temporary, uncommitted typescript.ignoreBuildErrors override so the Dockerfile itself could be exercised.

Size note: node:20-alpine alone is ~194 MB uncompressed and the backend's production dependency tree is ~516 MB, so the < 150 MB target is not reachable for the backend without dependency pruning/bundling — suggested as a follow-up.

Test plan

docker compose build
docker compose up -d
curl -fsS http://localhost:3001/health
curl -fsS http://localhost:3000/health
docker inspect --format '{{.State.Health.Status}}' flowfi-backend flowfi-frontend

…dalone (LabsCrypt#1335)

Backend runner now drops to the non-root `node` user, adds a native
/health HEALTHCHECK, runs `node` directly as PID 1 for graceful SIGTERM
handling, and purges the npm cache. Copied artifacts are chowned during
COPY so no extra image layer is created.

Frontend gains a dedicated multi-stage Dockerfile built on Next.js
`output: "standalone"` (with a new dependency-free /health route), so the
runtime image ships only the traced bundle instead of the whole workspace
node_modules. A repo-root .dockerignore keeps that build context lean.

docker-compose now builds and runs the frontend, wires depends_on to the
backend's health probe, and probes 127.0.0.1 (busybox wget resolves
`localhost` to IPv6 first, which the IPv4-bound servers do not answer).
The hardened runner installs production deps with --ignore-scripts, which
skips the @prisma/engines postinstall, so the Prisma schema-engine binary was
missing and `prisma db push` tried to download it at runtime into root-owned
node_modules. The container runs as the non-root `node` user, so the migration
step in the Backend Docker Image CI job failed with "Can't write to
/app/node_modules/@prisma/engines".

Run the engines postinstall explicitly as root at build time so the binary
ships in the image and the runtime user only reads it, preserving the
non-root, production-only hardening.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The main->branch merge (7535117) left docker-compose.yml invalid YAML:
main's node-based backend healthcheck was appended to the frontend
service, so the frontend healthcheck carried duplicate `test`, `interval`,
`timeout`, `retries` and `start_period` keys. `docker compose build` then
failed to parse the file and Backend Docker Image CI died at the Docker
Compose Build step before any image was built.

Restore the backend healthcheck to main's node-based form (the merge had
reverted it to the older wget version) and leave the frontend with a
single wget healthcheck on port 3000, so merging both parents yields one
valid, working healthcheck per service.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…#1335)

The hardened backend image runs as the non-root `node` user, so the CI
health-check's `prisma db push` failed when its default post-step
`prisma generate` tried to write engines into the root-owned
node_modules tree ("Can't write to /app/node_modules/prisma"). The
generated client is already baked into dist/generated by the builder
stage, so pass --skip-generate to run only the schema push.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@K1NGD4VID
K1NGD4VID merged commit 231cb33 into LabsCrypt:main Oct 8, 2026
12 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.

[DevOps] Docker Multi-Stage Production Build Optimization & Security Hardening

2 participants