From 1a1f313bf7edbb8b679504ce47bed4454242416e Mon Sep 17 00:00:00 2001 From: Nishant-S-Bhardwaj Date: Mon, 31 Aug 2026 12:48:14 +0530 Subject: [PATCH 1/2] docker: optimize backend/frontend images for #1335 (non-root, healthchecks, size reduction - see PR description for details) --- .dockerignore | 8 ++++++ backend/Dockerfile | 49 +++++++++++--------------------- frontend/.dockerignore | 8 ++++++ frontend/Dockerfile | 25 ++++++++++++++++ frontend/next.config.ts | 4 +++ frontend/src/app/health/route.ts | 5 ++++ 6 files changed, 67 insertions(+), 32 deletions(-) create mode 100644 .dockerignore create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/src/app/health/route.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5ab2e907 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +**/node_modules +**/.next +.git +.github +coverage +*.log +.env +.env.* diff --git a/backend/Dockerfile b/backend/Dockerfile index f8f8a1cf..e806d710 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,52 +1,37 @@ FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS builder - WORKDIR /app -# Copy the prisma directory up front so that (a) any future postinstall -# hook from @prisma/client sees the schema, and (b) the prebuild script -# `prisma generate` fired by `npm run build` below can find it. Doing -# the layout this way means the only required write is the generated -# client under src/generated/prisma, which is already covered by the -# later `COPY src ./src` step below + the runner-stage copy. COPY package*.json ./ COPY prisma ./prisma - RUN npm install COPY tsconfig.json ./ COPY prisma.config.ts ./ COPY src ./src - RUN npm run build FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS runner - WORKDIR /app - ENV NODE_ENV=production -# Order matters here for layer caching: COPY package*.json + npm install -# stay pinned together so that pure source-only changes do NOT invalidate -# the dependency install. --ignore-scripts skips the @prisma/client -# postinstall (we already COPY the generated client from the builder -# below), so the runner never needs the schema to install dependencies. -# The schema is then copied AFTER install so it is on disk for the -# `npx prisma db push` step the workflow runs against this container, -# while keeping the install layer cached whenever prisma/ is unchanged. -COPY package*.json ./ -RUN npm install --omit=dev --ignore-scripts +# /app is created by root via WORKDIR. Give node ownership of the +# directory itself (cheap metadata change, empty dir) BEFORE switching +# users, so npm can create node_modules without a permission error. +RUN chown node:node /app -# Copy the prisma schema into the runner. The boot-and-check-health step -# in .github/workflows/ci.yml runs `npx prisma db push` against this -# container; without prisma/schema.prisma on disk, that command errors -# out and the 60-iteration /health poll times the job out. -COPY prisma ./prisma +COPY --chown=node:node package*.json ./ -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/src/generated ./dist/generated -COPY --from=builder /app/prisma ./prisma -COPY prisma.config.ts ./ +USER node -EXPOSE 3001 +RUN npm install --omit=dev --ignore-scripts && npm cache clean --force -CMD ["npm", "start"] \ No newline at end of file +COPY --chown=node:node prisma ./prisma +COPY --from=builder --chown=node:node /app/dist ./dist +COPY --from=builder --chown=node:node /app/src/generated ./dist/generated +COPY --chown=node:node prisma.config.ts ./ + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD node -e "const http=require('http');const req=http.request({host:'127.0.0.1',port:3001,path:'/health',method:'GET',timeout:2000},res=>{process.exit(res.statusCode===200?0:1)});req.on('error',()=>process.exit(1));req.end();" + +EXPOSE 3001 +CMD ["npm", "start"] diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..8bd33142 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.next +.git +.github +coverage +*.log +.env +.env.* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..a7561c3c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS runner +WORKDIR /app +ENV NODE_ENV=production + +RUN chown node:node /app +USER node + +# Standalone output includes a minimal server.js + only the node_modules +# actually required at runtime — no need for a separate npm install here. +COPY --from=builder --chown=node:node /app/public ./public +COPY --from=builder --chown=node:node /app/.next/standalone ./ +COPY --from=builder --chown=node:node /app/.next/static ./.next/static + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD node -e "const http=require('http');const req=http.request({host:'127.0.0.1',port:3000,path:'/health',method:'GET',timeout:2000},res=>{process.exit(res.statusCode===200?0:1)});req.on('error',()=>process.exit(1));req.end();" + +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/frontend/next.config.ts b/frontend/next.config.ts index c90d14b9..74205350 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,12 +2,16 @@ import path from "node:path"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { +<<<<<<< HEAD // The workspace root lives one level above this directory. Pinning it here // prevents Turbopack from inferring a wrong root when stray package-lock // files exist outside the repo (e.g. ~/package-lock.json). turbopack: { root: path.join(path.dirname(new URL(import.meta.url).pathname), ".."), }, +======= + output: 'standalone', +>>>>>>> 90c0dc1 (docker: optimize backend/frontend images for #1335 (non-root, healthchecks, size reduction - see PR description for details)) // Enable tree-shaking for icon/utility libraries to reduce per-route // bundle sizes (Issue #1254). experimental: { diff --git a/frontend/src/app/health/route.ts b/frontend/src/app/health/route.ts new file mode 100644 index 00000000..35adb415 --- /dev/null +++ b/frontend/src/app/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ status: "ok" }, { status: 200 }); +} From 019e5cdfa027fae71dc152c16b4645d9928287f8 Mon Sep 17 00:00:00 2001 From: Nishant-S-Bhardwaj Date: Mon, 31 Aug 2026 13:46:34 +0530 Subject: [PATCH 2/2] fix: resolve frontend config merge conflict --- frontend/next.config.ts | 13 ++++++------- package-lock.json | 11 ++++++----- package.json | 1 + 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 74205350..0dd1068c 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,16 +2,14 @@ import path from "node:path"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { -<<<<<<< HEAD - // The workspace root lives one level above this directory. Pinning it here - // prevents Turbopack from inferring a wrong root when stray package-lock - // files exist outside the repo (e.g. ~/package-lock.json). + output: "standalone", + + // The workspace root lives one level above this directory. + // Pin it to prevent Turbopack from inferring the wrong root. turbopack: { root: path.join(path.dirname(new URL(import.meta.url).pathname), ".."), }, -======= - output: 'standalone', ->>>>>>> 90c0dc1 (docker: optimize backend/frontend images for #1335 (non-root, healthchecks, size reduction - see PR description for details)) + // Enable tree-shaking for icon/utility libraries to reduce per-route // bundle sizes (Issue #1254). experimental: { @@ -21,6 +19,7 @@ const nextConfig: NextConfig = { "@tanstack/react-virtual", ], }, + async redirects() { return [ { diff --git a/package-lock.json b/package-lock.json index abf30dcf..42fdc3dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react-hot-toast": "^2.6.0" }, "devDependencies": { + "@playwright/test": "^1.62.1", "@vitest/coverage-v8": "^3.2.7", "husky": "^9.1.7", "lint-staged": "^17.0.7", @@ -455,7 +456,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -465,7 +466,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -499,7 +500,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -559,7 +560,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -9260,7 +9261,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.25.4", diff --git a/package.json b/package.json index c63c7fb5..130781d0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "verify-security": "node scripts/verify-security-setup.js" }, "devDependencies": { + "@playwright/test": "^1.62.1", "@vitest/coverage-v8": "^3.2.7", "husky": "^9.1.7", "lint-staged": "^17.0.7",