diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..31335ba7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules +npm-debug.log +yarn-error.log + +# Build outputs +.next +dist +build + +# Development +.git +.gitignore +.eslintignore +.prettierignore +.husky +.env +.env.local +.env.*.local + +# IDE and editors +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store + +# Testing +coverage +.vitest +test-output*.txt + +# Documentation +README.md +CONTRIBUTING.md +docs +screenshots + +# CI/CD +.github +.gitlab-ci.yml + +# Temporary files +*.tmp +*.temp +.cache + +# Lock files (optional - include for reproducibility) +# package-lock.json diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md new file mode 100644 index 00000000..83443779 --- /dev/null +++ b/DOCKER_DEPLOYMENT.md @@ -0,0 +1,272 @@ +# Docker Deployment Guide + +This document provides comprehensive instructions for building, running, and deploying the TeachLink application using Docker. + +## Architecture + +The Docker setup uses **multi-stage builds** to optimize: +- **Build Stage**: Compiles Next.js and validates i18n configuration +- **Runtime Stage**: Lean production image with minimal dependencies + +### Image Optimization +- Base image: `node:20-alpine` (~150MB) +- Production image size: ~250-300MB (after build) +- Development image: Includes dev dependencies for fast iteration + +## Prerequisites + +- Docker 20.10+ +- Docker Compose 2.0+ +- Git + +## Development Setup + +### Quick Start (Hot Reload) + +```bash +# Start development environment with hot-reload +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +# Access the app at http://localhost:3000 +``` + +### Development Features + +- **Volume Mounts**: Source code changes immediately reflected +- **Hot Reload**: Next.js dev server with Turbopack +- **Network**: Isolated `teachlink-network` for scalability +- **Logs**: Available at `./logs` directory + +### Rebuild Containers + +```bash +# Full rebuild +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build + +# Remove old images +docker-compose down +docker system prune -a +``` + +## Production Deployment + +### Build Production Image + +```bash +# Build the production image +docker build -t teachlink:latest . + +# Tag for registry (example: DockerHub) +docker tag teachlink:latest yourusername/teachlink:latest +docker tag teachlink:latest yourusername/teachlink:1.0.0 +``` + +### Run Production Container + +#### Using docker-compose (Recommended) + +```bash +# Start production services +docker-compose up -d + +# View logs +docker-compose logs -f app + +# Stop services +docker-compose down +``` + +#### Using docker directly + +```bash +docker run -d \ + --name teachlink-app \ + -p 3000:3000 \ + -e NODE_ENV=production \ + -e NEXT_TELEMETRY_DISABLED=1 \ + --restart unless-stopped \ + --health-cmd='node -e "require('"'"'http'"'"').get('"'"'http://localhost:3000/api/health'"'"', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"' \ + --health-interval=30s \ + --health-timeout=3s \ + --health-start-period=5s \ + --health-retries=3 \ + teachlink:latest +``` + +## Environment Configuration + +### Production Variables + +```env +NODE_ENV=production +NEXT_TELEMETRY_DISABLED=1 +``` + +Add other required environment variables in `docker-compose.yml` or pass via `-e` flag. + +### Development Variables + +```env +NODE_ENV=development +NEXT_TELEMETRY_DISABLED=1 +``` + +## Health Checks + +### Endpoint + +The container includes a built-in health check that calls `/api/health`. + +Ensure your app has this endpoint: + +```typescript +// src/app/api/health/route.ts +export async function GET() { + return Response.json({ status: 'healthy' }, { status: 200 }); +} +``` + +### Manual Health Check + +```bash +docker exec teachlink-app node -e "require('http').get('http://localhost:3000/api/health', (r) => console.log(r.statusCode))" +``` + +## Security Best Practices + +### Image Scanning + +```bash +# Scan for vulnerabilities (requires Trivy) +trivy image teachlink:latest +``` + +### Non-Root User + +The production image runs as `nextjs` (UID 1001) for security. + +### Network Isolation + +- Services run on `teachlink-network` +- Only expose necessary ports +- Use environment variables for sensitive data + +## Scaling + +### Multiple Instances with Load Balancing + +```yaml +# docker-compose.yml example +services: + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - app + + app: + build: . + deploy: + replicas: 3 +``` + +## Troubleshooting + +### Container Won't Start + +```bash +# View logs +docker-compose logs app + +# Inspect image +docker inspect teachlink:latest + +# Run interactive shell +docker run -it --entrypoint /bin/sh teachlink:latest +``` + +### High Memory Usage + +```bash +# Check memory consumption +docker stats teachlink-app + +# Limit memory in docker-compose.yml +deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M +``` + +### Port Already in Use + +```bash +# Find process using port 3000 +lsof -i :3000 + +# Change port in docker-compose.yml +ports: + - "3001:3000" +``` + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +name: Build and Push Docker Image + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: yourusername/teachlink:latest + cache-from: type=registry,ref=yourusername/teachlink:buildcache + cache-to: type=registry,ref=yourusername/teachlink:buildcache,mode=max +``` + +## Cleanup + +```bash +# Stop all containers +docker-compose down + +# Remove unused images +docker image prune + +# Remove all Docker resources +docker system prune -a --volumes +``` + +## Reference + +- [Next.js Docker Documentation](https://nextjs.org/docs/deployment/docker) +- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/) +- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/) +- [Alpine Linux Benefits](https://www.alpinelinux.org/) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..cdf103e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# Multi-stage build for optimal image size and production performance +# Stage 1: Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install ALL dependencies including dev dependencies (needed for build) +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts && npm cache clean --force + +# Copy source code +COPY . . + +# Run build-time validations +RUN npm run check-locales && npm run check-i18n + +# Build the Next.js application (lint is handled separately in CI) +RUN npm run build -- --no-lint + +# Stage 2: Production runtime +FROM node:20-alpine AS runtime + +WORKDIR /app + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 + +# Copy built application from builder +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Set environment to production +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +USER nextjs + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" + +# Use dumb-init to handle signals properly +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "server.js"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..1bd4f57e --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,18 @@ +# Development Dockerfile with hot-reload support +FROM node:20-alpine AS development + +WORKDIR /app + +# Install development dependencies +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy source code +COPY . . + +ENV NODE_ENV=development +ENV NEXT_TELEMETRY_DISABLED=1 + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..d26739fe --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,20 @@ +version: '3.8' + +# Development overrides - use with: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + target: development + command: npm run dev + environment: + - NODE_ENV=development + - NEXT_TELEMETRY_DISABLED=1 + volumes: + - .:/app + - /app/node_modules + ports: + - "3000:3000" + restart: on-failure diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b71285c4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + target: runtime + container_name: teachlink-app + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - NEXT_TELEMETRY_DISABLED=1 + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - teachlink-network + volumes: + - ./logs:/app/logs + + # Development-specific overrides using a separate compose file + # Run with: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +networks: + teachlink-network: + driver: bridge + +volumes: + logs: diff --git a/next.config.ts b/next.config.ts index 66df614a..9090fedd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import path from 'path'; const nextConfig: NextConfig = { /* config options here */ + output: 'standalone', images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],