diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9095be1a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,55 @@ +# Docker ignore rules for flowfi +# Exclude files that inflate image context size but aren't needed at build time. + +# Version control +.git +.gitignore +.gitattributes + +# Dependency directories (re-installed inside the container) +node_modules +**/node_modules + +# Build outputs (rebuilt inside the container) +dist +build +.next +out + +# Test artifacts +coverage +*.test.* +*.spec.* +__tests__ +__mocks__ +test +tests +e2e + +# Documentation +*.md +docs + +# Environment / secrets +.env +.env.* +!.env.example + +# Editor / IDE config +.vscode +.idea +*.swp +*.swo +.DS_Store + +# CI +.github + +# Logs +*.log +logs +npm-debug.log* + +# Misc +*.tmp +*.bak diff --git a/backend/Dockerfile b/backend/Dockerfile index f8f8a1cf..dc8bb97c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,3 +1,6 @@ +# ============================================================ +# Stage 1: builder — full Node toolchain to compile TypeScript +# ============================================================ FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS builder WORKDIR /app @@ -11,7 +14,8 @@ WORKDIR /app COPY package*.json ./ COPY prisma ./prisma -RUN npm install +# Install all deps (incl. devDependencies needed for tsc / prisma generate) +RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi COPY tsconfig.json ./ COPY prisma.config.ts ./ @@ -19,8 +23,15 @@ COPY src ./src RUN npm run build +# ============================================================ +# Stage 2: production runner — minimal Alpine image, non-root +# ============================================================ FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS runner +# Security: drop to non-root user for the running process +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 --ingroup nodejs nodeapp + WORKDIR /app ENV NODE_ENV=production @@ -34,7 +45,7 @@ ENV NODE_ENV=production # `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 +RUN if [ -f package-lock.json ]; then npm ci --omit=dev --ignore-scripts; else npm install --omit=dev --ignore-scripts; fi # 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 @@ -47,6 +58,16 @@ COPY --from=builder /app/src/generated ./dist/generated COPY --from=builder /app/prisma ./prisma COPY prisma.config.ts ./ +# Ownership hand-off — ensure the non-root user owns all app files +RUN chown -R nodeapp:nodejs /app + +USER nodeapp + EXPOSE 3001 +# Health check so orchestrators (Docker Compose / Kubernetes) can detect +# when the API is ready to serve traffic. +HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=5 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1 + CMD ["npm", "start"] \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..67e2e679 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,61 @@ +# ============================================================ +# Stage 1: deps — install production-only dependencies +# ============================================================ +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS deps + +WORKDIR /app + +# Copy lockfile first for optimal Docker layer caching: +# rebuilds only when package files change, not source files. +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi + +# ============================================================ +# Stage 2: builder — compile Next.js application +# ============================================================ +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi + +# Copy source files +COPY . . + +# Build the Next.js application (output: .next directory) +RUN npm run build + +# ============================================================ +# Stage 3: production runner — minimal Alpine image, non-root +# ============================================================ +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS runner + +# Security: run as non-root user +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 --ingroup nodejs nextapp + +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 + +# Copy only what is needed to run the app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/next.config.ts ./ + +# Ownership hand-off — ensure the non-root user owns all app files +RUN chown -R nextapp:nodejs /app + +USER nextapp + +EXPOSE 3000 + +# Health check so orchestrators know when Next.js is ready to serve +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 + +CMD ["npm", "start"]