Repository navigation
feat(devops): Docker multi-stage production build optimization & security hardening (#1335) - #1582
Merged
K1NGD4VID merged 6 commits intoOct 8, 2026
Conversation
…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>
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.
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/Dockerfilebuilder → runnersplit on the pinnednode:20-alpinedigest.nodeuser (USER node).HEALTHCHECKpollinghttp://127.0.0.1:3001/health.CMD ["node", "dist/index.js"]instead ofnpm start, so the server is PID 1 and receivesSIGTERM/SIGINTfor the graceful-shutdown handler insrc/index.ts.COPY --chown=node:node(no extrachown -Rlayer),npm cache clean, dropped the redundant duplicateprismacopy.Frontend —
frontend/Dockerfile(new) +frontend/next.config.tsoutput: "standalone"(the issue's requirement)..next/standalone+.next/static+public— never the workspacenode_modules.nodeuser + nativeHEALTHCHECKon/health.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.dockerignorefrontendservice with build context at the repo root so the standalone monorepo layout (frontend/server.js) is preserved;depends_on: backend (healthy).start_periodfor warm-up.127.0.0.1deliberately: busyboxwgetresolveslocalhostto IPv6::1first, which the IPv4-bound servers do not answer..dockerignorekeeps the context lean (.git,node_modules, build output, tests, docs, contracts).Verification
Frontend image built and run locally:
docker build -f frontend/Dockerfile .→ successState.Health.Status→healthy;GET /health→200 {"status":"ok"};GET /→200uid=1000(node)Backend Dockerfile parses and its dependency/COPY stages run; the final
npm run buildstep fails on a pre-existingprisma/tscbreakage onmain(see below), not on the Dockerfile itself.Pre-existing blockers on
main(NOT introduced here)While verifying I found
mainis already broken in ways unrelated to #1335:backend/prisma/schema.prismadeclaresIndexerDeadLetterEventtwice, and two migrations create the same table (bad merge38a3e2d).prisma generatetherefore fails → the backend image cannot build until this is resolved.backend/package.jsonrequires@stellar/stellar-sdk@^17.0.1while the committed lockfile resolves15.1.0(commit4a8009c), and the code was not migrated to the v17 API — sonpm cifails the sync check andtscreports 76 errors in the backend / 19 in the frontend.next buildcurrently fails on pre-existing frontend type errors; the frontend image was validated with a temporary, uncommittedtypescript.ignoreBuildErrorsoverride so the Dockerfile itself could be exercised.Size note:
node:20-alpinealone 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