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..0dd1068c 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,12 +2,14 @@ import path from "node:path"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { - // 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), ".."), }, + // Enable tree-shaking for icon/utility libraries to reduce per-route // bundle sizes (Issue #1254). experimental: { @@ -17,6 +19,7 @@ const nextConfig: NextConfig = { "@tanstack/react-virtual", ], }, + async redirects() { return [ { 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 }); +} diff --git a/package-lock.json b/package-lock.json index 82d2a65f..a6aa6e60 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", diff --git a/package.json b/package.json index 1ded9a4a..5c72c018 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",