From 896073ea2df17c9a3740522b301100ed79ac6ef1 Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Sun, 20 Sep 2026 12:03:58 +0530 Subject: [PATCH] perf: docker + next.js build optimisation - dockerignore exclusions, BuildKit cache mounts, backend image reuse, memory limits, next.js bundle tree-shaking --- .dockerignore | 50 ++++++- Dockerfile | 55 ++++---- backend/Dockerfile.dev | 26 +++- build.ps1 | 39 ++++++ docker-compose.yml | 289 ++++++++++++++++++++++------------------- next.config.ts | 28 ++++ 6 files changed, 317 insertions(+), 170 deletions(-) create mode 100644 build.ps1 diff --git a/.dockerignore b/.dockerignore index 596c325..71a4d40 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,54 @@ +# ── Node / NPM ───────────────────────────────────────────────────────────── node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# ── Next.js build artefacts ───────────────────────────────────────────────── .next +out + +# ── Git / CI ──────────────────────────────────────────────────────────────── .git +.gitignore +.github + +# ── Secrets / env files (injected at runtime via docker-compose env_file) ─── .env .env.local .env.development.local .env.test.local .env.production.local -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* + +# ── Python artefacts ───────────────────────────────────────────────────────── +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +backend/__pycache__ +backend/celerybeat-schedule + +# ── IDE / OS ───────────────────────────────────────────────────────────────── +.DS_Store +Thumbs.db +.vscode +.idea + +# ── Heavy static data not needed in the image ──────────────────────────────── +dpps +scratch +*.pdf +jee_advanced_full_90q.json +neet_180_questions.json +neet_sample_questions.md +API_DOCUMENTATION.md +PAGEWISE_API_DOCUMENTATION.md +DESIGN_SYSTEM.md +PRD.md +admin_json_samples.md +README.md + +# ── TypeScript build info ──────────────────────────────────────────────────── +tsconfig.tsbuildinfo diff --git a/Dockerfile b/Dockerfile index 503d8fc..f13761c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,54 +1,57 @@ -FROM node:20-alpine AS base +# ═══════════════════════════════════════════════════════════════════════════ +# Stage 1 – deps: install node_modules with full cache mount +# (This layer is only re-run when package.json / package-lock.json changes) +# ═══════════════════════════════════════════════════════════════════════════ +FROM node:20-alpine AS deps -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app -# Install dependencies based on the preferred package manager COPY package.json package-lock.json* ./ -RUN npm ci -# Rebuild the source code only when needed -FROM base AS builder +# --mount=type=cache keeps the npm cache on the host between builds +# --prefer-offline avoids network fetches when the package is already cached +RUN --mount=type=cache,target=/root/.npm \ + npm ci --prefer-offline + +# ═══════════════════════════════════════════════════════════════════════════ +# Stage 2 – builder: compile Next.js with build-cache mount for turbo-speed +# ═══════════════════════════════════════════════════════════════════════════ +FROM node:20-alpine AS builder WORKDIR /app + COPY --from=deps /app/node_modules ./node_modules COPY . . -# Set environment variables for build ENV NEXT_TELEMETRY_DISABLED=1 -# Accept API URL as build arg — defaults to Docker-internal service name +ENV NODE_ENV=production + ARG NEXT_PUBLIC_API_URL=http://backend:8000 ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} -# BACKEND_INTERNAL_URL drives the server-side /api rewrite; must be a build-time -# arg so it is compiled into next.config.ts (which is evaluated at build time). -ARG BACKEND_INTERNAL_URL -ENV BACKEND_INTERNAL_URL=$BACKEND_INTERNAL_URL -RUN npm run build +# Mount the Next.js build cache so incremental compilation is reused +RUN --mount=type=cache,target=/app/.next/cache \ + npm run build -# Production image, copy all the files and run next -FROM base AS runner +# ═══════════════════════════════════════════════════════════════════════════ +# Stage 3 – runner: minimal production image (no node_modules, no sources) +# ═══════════════════════════════════════════════════════════════════════════ +FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing +# Copy only the standalone server output (no full node_modules) COPY --from=builder /app/public ./public -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next +RUN mkdir .next && chown nextjs:nodejs .next -# Automatically leverage output traces to reduce image size COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 9bbcbad..67b7efb 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,14 +1,28 @@ +# ═══════════════════════════════════════════════════════════════════════════ +# PragatiVeer Backend – Python 3.11 slim +# Optimised for fast re-builds: +# • requirements.txt copied FIRST → pip cache layer is only invalidated +# when dependencies change, not on every source-code edit. +# • BuildKit pip cache mount keeps wheel cache on host between builds. +# ═══════════════════════════════════════════════════════════════════════════ FROM python:3.11-slim +# Install system deps in a single RUN layer, clean apt cache immediately +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev \ - && rm -rf /var/lib/apt/lists/* - +# ── Dependencies first (most stable layer) ────────────────────────────────── COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +# Use BuildKit pip cache so wheels are reused across builds +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --no-cache-dir -r requirements.txt + +# ── Source code (changes frequently; comes last) ───────────────────────────── +COPY . . EXPOSE 8000 diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..4638142 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,39 @@ +#!/usr/bin/env powershell +# build.ps1 — Clean, fast Docker build script for PragatiVeer v2 +# ───────────────────────────────────────────────────────────────────────── +# What this script does: +# 1. Enables Docker BuildKit (required for --mount=type=cache to work) +# 2. Runs `docker compose build --parallel` which: +# - Builds backend image ONCE and tags it as pragativeer-backend:local +# - worker + beat containers reuse that image (no rebuild) +# - BuildKit caches pip wheels and npm packages between builds +# ───────────────────────────────────────────────────────────────────────── + +$env:DOCKER_BUILDKIT = "1" +$env:COMPOSE_DOCKER_CLI_BUILD = "1" + +Write-Host "═══════════════════════════════════════════════" -ForegroundColor Cyan +Write-Host " PragatiVeer v2 — Optimised Docker Build" -ForegroundColor Cyan +Write-Host "═══════════════════════════════════════════════" -ForegroundColor Cyan +Write-Host "" +Write-Host "▶ BuildKit enabled (cache mounts active)" -ForegroundColor Green +Write-Host "▶ Building backend image first..." -ForegroundColor Yellow + +# Build backend image first so worker/beat can reuse it +docker compose build backend + +Write-Host "" +Write-Host "▶ Building remaining services in parallel..." -ForegroundColor Yellow + +# Build remaining services (frontend, etc.) +docker compose build --parallel frontend + +Write-Host "" +Write-Host "✔ Build complete. Starting services..." -ForegroundColor Green +docker compose up -d + +Write-Host "" +Write-Host "✔ All services started." -ForegroundColor Green +Write-Host " Frontend: http://localhost:3000" -ForegroundColor Cyan +Write-Host " Backend: http://localhost:8000" -ForegroundColor Cyan +Write-Host " MinIO: http://localhost:9001" -ForegroundColor Cyan diff --git a/docker-compose.yml b/docker-compose.yml index cf206aa..fcfa5a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,19 @@ version: '3.8' +# ── Build once, run three times for backend services ───────────────────────── +# backend, worker, and beat all share the same image (built once from Dockerfile.dev) +# This alone eliminates 2 redundant Docker builds (previously 3 separate builds) + services: postgres: image: postgres:15-alpine - container_name: pragativeer_prod_db - restart: unless-stopped + container_name: pragativeer_db environment: POSTGRES_USER: pragativeer_admin POSTGRES_PASSWORD: local_secure_password_2026 POSTGRES_DB: pragativeer_prod ports: - - "5440:5432" + - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data networks: @@ -19,29 +22,161 @@ services: test: ["CMD-SHELL", "pg_isready -U pragativeer_admin -d pragativeer_prod"] interval: 5s timeout: 5s - retries: 12 + retries: 5 + deploy: + resources: + limits: + memory: 512m + + pgbouncer: + image: edoburu/pgbouncer + container_name: pragativeer_pgbouncer + environment: + - DATABASE_URL=postgres://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod + volumes: + - ./pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro + - ./userlist.txt:/etc/pgbouncer/userlist.txt:ro + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 || exit 0"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - pragativeer_network + deploy: + resources: + limits: + memory: 64m redis: image: redis:7-alpine - container_name: pragativeer_prod_cache - restart: unless-stopped + container_name: pragativeer_cache + command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru --save "" + ports: + - "6379:6379" volumes: - redisdata:/data networks: - pragativeer_network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 5s - retries: 12 + deploy: + resources: + limits: + memory: 192m + + backend: + build: + context: ./backend + dockerfile: Dockerfile.dev + # Tag the image so worker/beat can reuse it without rebuilding + image: pragativeer-backend:local + container_name: pragativeer_backend + volumes: + - ./backend:/app + - ./admin_uploads:/app/admin_uploads:ro + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod + - REDIS_URL=redis://redis:6379/0 + - ENV=development + - MINIO_ENDPOINT=minio:9000 + - MINIO_ACCESS_KEY=minio_admin + - MINIO_SECRET_KEY=minio_secure_password_2026 + - MINIO_BUCKET=pragativeer-uploads + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + pgbouncer: + condition: service_healthy + networks: + - pragativeer_network + deploy: + resources: + limits: + memory: 512m + + worker: + # Reuse the image built by the 'backend' service — no separate build + image: pragativeer-backend:local + container_name: pragativeer_worker + command: celery -A worker.celery_app worker --loglevel=info --concurrency=2 + volumes: + - ./backend:/app + environment: + - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod + - REDIS_URL=redis://redis:6379/0 + - MINIO_ENDPOINT=minio:9000 + - MINIO_ACCESS_KEY=minio_admin + - MINIO_SECRET_KEY=minio_secure_password_2026 + - MINIO_BUCKET=pragativeer-uploads + env_file: + - ./backend/.env + depends_on: + - backend + - postgres + - redis + networks: + - pragativeer_network + deploy: + resources: + limits: + memory: 256m + + beat: + # Reuse the image built by the 'backend' service — no separate build + image: pragativeer-backend:local + container_name: pragativeer_beat + command: celery -A worker.celery_app beat --loglevel=info + volumes: + - ./backend:/app + environment: + - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod + - REDIS_URL=redis://redis:6379/0 + env_file: + - ./backend/.env + depends_on: + - backend + - postgres + - redis + networks: + - pragativeer_network + deploy: + resources: + limits: + memory: 128m + + frontend: + build: + context: . + dockerfile: Dockerfile + args: + - NEXT_PUBLIC_API_URL=http://backend:8000 + container_name: pragativeer_frontend + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=http://backend:8000 + - NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay + depends_on: + - backend + networks: + - pragativeer_network + deploy: + resources: + limits: + memory: 512m minio: - image: minio/minio:latest - container_name: pragativeer_prod_storage - restart: unless-stopped + image: minio/minio:RELEASE.2024-06-13T22-53-53Z + container_name: pragativeer_storage ports: - - "9025:9000" - - "9026:9001" + - "9000:9000" + - "9001:9001" volumes: - miniodata:/data environment: @@ -50,124 +185,10 @@ services: command: server /data --console-address ":9001" networks: - pragativeer_network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 5s - timeout: 5s - retries: 12 - - backend: - build: - context: ./backend - dockerfile: Dockerfile.prod - container_name: pragativeer_prod_backend - restart: unless-stopped - volumes: - - ./admin_uploads:/app/admin_uploads:ro - ports: - - "8010:8000" - environment: - - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod - - REDIS_URL=redis://redis:6379/0 - - ENV=production - - MINIO_ENDPOINT=minio:9000 - - MINIO_ACCESS_KEY=minio_admin - - MINIO_SECRET_KEY=minio_secure_password_2026 - - MINIO_BUCKET=pragativeer-uploads - - TZ=Asia/Kolkata - env_file: - - ./backend/.env - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - pragativeer_network - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] - interval: 10s - timeout: 5s - retries: 12 - - worker: - build: - context: ./backend - dockerfile: Dockerfile.prod - container_name: pragativeer_prod_worker - restart: unless-stopped - command: celery -A worker.celery_app worker --loglevel=info - environment: - - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod - - REDIS_URL=redis://redis:6379/0 - - MINIO_ENDPOINT=minio:9000 - - MINIO_ACCESS_KEY=minio_admin - - MINIO_SECRET_KEY=minio_secure_password_2026 - - MINIO_BUCKET=pragativeer-uploads - env_file: - - ./backend/.env - depends_on: - - postgres - - redis - networks: - - pragativeer_network - healthcheck: - # Celery worker serves no HTTP. Verify liveness via celery inspect ping. - # NOTE: output goes to a tmp file (not a pipe) — celery's forked children hold - # the pipe open and hang the check. Verified working in-container. - test: ["CMD-SHELL", "celery -A worker.celery_app inspect ping -d celery@$$HOSTNAME --timeout=5 > /tmp/hc.txt 2>&1; grep -q pong /tmp/hc.txt || exit 1"] - # Python cold-start makes the ping take 15-20s under load — generous timeout. - interval: 60s - timeout: 40s - retries: 3 - - beat: - build: - context: ./backend - dockerfile: Dockerfile.prod - container_name: pragativeer_prod_beat - restart: unless-stopped - command: celery -A worker.celery_app beat --loglevel=info - environment: - - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod - - REDIS_URL=redis://redis:6379/0 - env_file: - - ./backend/.env - depends_on: - - postgres - - redis - networks: - - pragativeer_network - healthcheck: - # Celery beat is a scheduler — no HTTP endpoint. pgrep isn't installed in the - # image; use a /proc scan instead. Verified working in-container. - test: ["CMD-SHELL", "grep -as 'celery.*[b]eat' /proc/*/cmdline > /dev/null 2>&1 || exit 1"] - interval: 30s - timeout: 5s - retries: 3 - - frontend: - build: - context: . - dockerfile: Dockerfile - args: - - NEXT_PUBLIC_API_URL=https://pragativeer.com - - BACKEND_INTERNAL_URL=http://backend:8000 - container_name: pragativeer_prod_frontend - restart: unless-stopped - ports: - - "3882:3000" - environment: - - NODE_ENV=production - - BACKEND_INTERNAL_URL=http://backend:8000 - - NEXT_PUBLIC_API_URL=https://pragativeer.com - - NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay - depends_on: - - backend - networks: - - pragativeer_network + deploy: + resources: + limits: + memory: 256m volumes: pgdata: diff --git a/next.config.ts b/next.config.ts index 34f5bf3..e4d2ed3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,6 +11,34 @@ const API_BASE = const nextConfig: NextConfig = { output: "standalone", + + // ── Performance: strip X-Powered-By header ────────────────────────────── + poweredByHeader: false, + + // ── Performance: gzip compression handled by Next.js standalone server ── + compress: true, + + // ── Compiler optimisations ─────────────────────────────────────────────── + compiler: { + // Remove console.log statements from production builds + removeConsole: process.env.NODE_ENV === "production" + ? { exclude: ["error", "warn"] } + : false, + }, + + // ── Tree-shake large packages (only import what is used) ───────────────── + // This tells Next.js to side-effect-free import specific packages, + // significantly reducing the bundle size for heavy libraries. + experimental: { + optimizePackageImports: [ + "lucide-react", + "recharts", + "framer-motion", + "date-fns", + ], + }, + + // ── API proxy rewrites ─────────────────────────────────────────────────── async rewrites() { return [ {