perf: docker + next.js build optimisation - dockerignore exclusions, BuildKit cache mounts, backend image reuse, memory limits, next.js bundle tree-shaking

This commit is contained in:
2026-09-20 12:03:58 +05:30
parent fee57e154b
commit 896073ea2d
6 changed files with 317 additions and 170 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

39
build.ps1 Normal file
View File

@@ -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

View File

@@ -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:

View File

@@ -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 [
{