diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..1a66635 --- /dev/null +++ b/.env.production.example @@ -0,0 +1,83 @@ +# ========================================== +# VIP Coordinator - Production Environment +# ========================================== +# Copy this file to .env.production and fill in your values +# DO NOT commit .env.production to version control + +# ========================================== +# Database Configuration +# ========================================== +POSTGRES_DB=vip_coordinator +POSTGRES_USER=vip_user +POSTGRES_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD + +# ========================================== +# Auth0 Configuration +# ========================================== +# Get these from your Auth0 dashboard: +# 1. Go to https://manage.auth0.com/ +# 2. Create or select your Application (Single Page Application) +# 3. Create or select your API +# 4. Copy the values below + +# Your Auth0 tenant domain (e.g., your-tenant.us.auth0.com) +AUTH0_DOMAIN=your-tenant.us.auth0.com + +# Your Auth0 API audience/identifier (e.g., https://vip-coordinator-api) +AUTH0_AUDIENCE=https://your-api-identifier + +# Your Auth0 issuer URL (usually https://your-tenant.us.auth0.com/) +AUTH0_ISSUER=https://your-tenant.us.auth0.com/ + +# Your Auth0 SPA Client ID (this is public, used in frontend) +AUTH0_CLIENT_ID=your-auth0-client-id + +# ========================================== +# Frontend Configuration +# ========================================== +# Port to expose the frontend on (default: 80) +FRONTEND_PORT=80 + +# API URL for frontend to use (default: http://localhost/api/v1) +# For production, this should be your domain's API endpoint +# Note: In containerized setup, /api is proxied by nginx to backend +VITE_API_URL=http://localhost/api/v1 + +# ========================================== +# Optional: External APIs +# ========================================== +# AviationStack API key for flight tracking (optional) +# Get one at: https://aviationstack.com/ +AVIATIONSTACK_API_KEY= + +# ========================================== +# Optional: Database Seeding +# ========================================== +# Set to 'true' to seed database with sample data on first run +# WARNING: Only use in development/testing environments +RUN_SEED=false + +# ========================================== +# Production Deployment Notes +# ========================================== +# 1. Configure Auth0: +# - Add callback URLs: https://your-domain.com/callback +# - Add allowed web origins: https://your-domain.com +# - Add allowed logout URLs: https://your-domain.com +# +# 2. For HTTPS/SSL: +# - Use a reverse proxy like Caddy, Traefik, or nginx-proxy +# - Or configure cloud provider's load balancer with SSL certificate +# +# 3. First deployment: +# docker-compose -f docker-compose.prod.yml up -d +# +# 4. To update: +# docker-compose -f docker-compose.prod.yml down +# docker-compose -f docker-compose.prod.yml build +# docker-compose -f docker-compose.prod.yml up -d +# +# 5. View logs: +# docker-compose -f docker-compose.prod.yml logs -f +# +# 6. Database migrations run automatically on backend startup diff --git a/.gitignore b/.gitignore index 811370a..b926364 100644 --- a/.gitignore +++ b/.gitignore @@ -81,9 +81,6 @@ frontend/e2e/ ehthumbs.db Thumbs.db -# Docker -.dockerignore - # Backup files *backup* *.bak diff --git a/README.md b/README.md index 63ef240..4061611 100644 --- a/README.md +++ b/README.md @@ -427,26 +427,113 @@ npx prisma db seed - [ ] Set up log aggregation - [ ] Configure CDN for frontend assets (optional) -### Docker Deployment +### Docker Deployment (Production-Ready) + +**Complete containerization with multi-stage builds, Nginx, and automated migrations.** + +#### Quick Start ```bash -# Build images -docker-compose build +# 1. Create production environment file +cp .env.production.example .env.production -# Start all services -docker-compose up -d +# 2. Edit .env.production with your values +# - Set strong POSTGRES_PASSWORD +# - Configure Auth0 credentials +# - Set AUTH0_CLIENT_ID for frontend -# Run migrations -docker-compose exec backend npx prisma migrate deploy +# 3. Build and start all services +docker-compose -f docker-compose.prod.yml up -d -# Seed database (optional) -docker-compose exec backend npx prisma db seed +# 4. Check service health +docker-compose -f docker-compose.prod.yml ps -# View logs -docker-compose logs -f backend -docker-compose logs -f frontend +# 5. View logs +docker-compose -f docker-compose.prod.yml logs -f ``` +#### What Gets Deployed + +- **PostgreSQL 16** - Database with persistent volume +- **Redis 7** - Caching layer with persistent volume +- **Backend (NestJS)** - Optimized production build (~200MB) + - Runs database migrations automatically on startup + - Non-root user for security + - Health checks enabled +- **Frontend (Nginx)** - Static files served with Nginx (~45MB) + - SPA routing configured + - API requests proxied to backend + - Gzip compression enabled + - Security headers configured + +#### First-Time Setup + +**Auth0 Configuration:** +1. Update callback URLs: `http://your-domain/callback` +2. Update allowed web origins: `http://your-domain` +3. Update logout URLs: `http://your-domain` + +**Access Application:** +- Frontend: `http://localhost` (or your domain) +- Backend health: `http://localhost/api/v1/health` + +#### Updating the Application + +```bash +# Pull latest code +git pull + +# Rebuild and restart +docker-compose -f docker-compose.prod.yml down +docker-compose -f docker-compose.prod.yml build --no-cache +docker-compose -f docker-compose.prod.yml up -d +``` + +#### Database Management + +```bash +# View migration status +docker-compose -f docker-compose.prod.yml exec backend npx prisma migrate status + +# Manually run migrations (not needed, runs automatically) +docker-compose -f docker-compose.prod.yml exec backend npx prisma migrate deploy + +# Seed database with test data (optional) +docker-compose -f docker-compose.prod.yml exec backend npx prisma db seed +``` + +#### Troubleshooting + +```bash +# Check container status +docker-compose -f docker-compose.prod.yml ps + +# View specific service logs +docker-compose -f docker-compose.prod.yml logs backend +docker-compose -f docker-compose.prod.yml logs frontend + +# Restart specific service +docker-compose -f docker-compose.prod.yml restart backend + +# Complete reset (⚠️ DELETES ALL DATA) +docker-compose -f docker-compose.prod.yml down -v +docker volume rm vip-coordinator-postgres-data vip-coordinator-redis-data +``` + +#### Production Enhancements + +For production deployment, add: +- **Reverse Proxy** (Caddy/Traefik) for SSL/TLS +- **Automated Backups** for PostgreSQL volumes +- **Monitoring** (Prometheus/Grafana) +- **Log Aggregation** (ELK/Loki) + +#### Image Sizes + +- Backend: ~200-250MB (multi-stage build) +- Frontend: ~45-50MB (nginx alpine) +- Total deployment: <300MB (excluding database volumes) + ### Environment Variables **Backend** (`backend/.env`) @@ -489,6 +576,9 @@ vip-coordinator/ │ │ ├── events/ # Activity scheduling (ScheduleEvent) │ │ ├── flights/ # Flight tracking │ │ └── common/ # Shared utilities, guards, decorators +│ ├── Dockerfile # Multi-stage production build +│ ├── docker-entrypoint.sh # Migration automation script +│ ├── .dockerignore # Docker build exclusions │ └── package.json │ ├── frontend/ # React Frontend @@ -500,10 +590,15 @@ vip-coordinator/ │ │ ├── hooks/ # Custom React hooks │ │ ├── lib/ # Utilities, API client │ │ └── types/ # TypeScript types +│ ├── Dockerfile # Multi-stage build with Nginx +│ ├── nginx.conf # Nginx server configuration +│ ├── .dockerignore # Docker build exclusions │ ├── playwright.config.ts # Playwright configuration │ └── package.json │ -├── docker-compose.yml # Docker orchestration +├── docker-compose.yml # Development environment (DB only) +├── docker-compose.prod.yml # Production deployment (full stack) +├── .env.production.example # Production environment template └── README.md # This file ``` diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..0279c4e --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,69 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json + +# Build output +dist +build +*.tsbuildinfo + +# Environment files (will be injected at runtime) +.env +.env.* +!.env.example + +# Testing +coverage +*.spec.ts +test +tests +**/__tests__ +**/__mocks__ + +# Documentation +*.md +!README.md +docs + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store + +# Git +.git +.gitignore +.gitattributes + +# Logs +logs +*.log + +# Temporary files +tmp +temp +*.tmp +*.temp + +# Docker files (avoid recursion) +Dockerfile* +.dockerignore +docker-compose*.yml + +# CI/CD +.github +.gitlab-ci.yml +.travis.yml + +# Misc +.editorconfig +.eslintrc* +.prettierrc* +tsconfig*.json +jest.config.js diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6e20def --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,87 @@ +# ========================================== +# Stage 1: Dependencies +# Install all dependencies and generate Prisma client +# ========================================== +FROM node:20-alpine AS dependencies + +# Install OpenSSL for Prisma support +RUN apk add --no-cache openssl libc6-compat + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including dev dependencies for build) +RUN npm ci + +# Copy Prisma schema and generate client +COPY prisma ./prisma +RUN npx prisma generate + +# ========================================== +# Stage 2: Builder +# Compile TypeScript application +# ========================================== +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy node_modules from dependencies stage +COPY --from=dependencies /app/node_modules ./node_modules + +# Copy application source +COPY . . + +# Build the application +RUN npm run build + +# Install only production dependencies +RUN npm ci --omit=dev && npm cache clean --force + +# ========================================== +# Stage 3: Production Runtime +# Minimal runtime image with only necessary files +# ========================================== +FROM node:20-alpine AS production + +# Install OpenSSL, dumb-init, and netcat for database health checks +RUN apk add --no-cache openssl dumb-init netcat-openbsd + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nestjs -u 1001 + +WORKDIR /app + +# Copy production dependencies from builder +COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules + +# Copy built application +COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist + +# Copy Prisma schema and migrations (needed for runtime) +COPY --from=builder --chown=nestjs:nodejs /app/prisma ./prisma + +# Copy package.json for metadata +COPY --from=builder --chown=nestjs:nodejs /app/package*.json ./ + +# Copy entrypoint script +COPY --chown=nestjs:nodejs docker-entrypoint.sh ./ +RUN chmod +x docker-entrypoint.sh + +# Switch to non-root user +USER nestjs + +# Expose application port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/v1/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Use dumb-init to handle signals properly +ENTRYPOINT ["/usr/bin/dumb-init", "--"] + +# Run entrypoint script (handles migrations then starts app) +CMD ["./docker-entrypoint.sh"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100644 index 0000000..f37f498 --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,85 @@ +#!/bin/sh +set -e + +echo "=== VIP Coordinator Backend - Starting ===" + +# Function to wait for PostgreSQL to be ready +wait_for_postgres() { + echo "Waiting for PostgreSQL to be ready..." + + # Extract host and port from DATABASE_URL + # Format: postgresql://user:pass@host:port/dbname + DB_HOST=$(echo $DATABASE_URL | sed -n 's/.*@\(.*\):.*/\1/p') + DB_PORT=$(echo $DATABASE_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') + + # Default to standard PostgreSQL port if not found + DB_PORT=${DB_PORT:-5432} + + echo "Checking PostgreSQL at ${DB_HOST}:${DB_PORT}..." + + # Wait up to 60 seconds for PostgreSQL + timeout=60 + counter=0 + + until nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do + counter=$((counter + 1)) + if [ $counter -gt $timeout ]; then + echo "ERROR: PostgreSQL not available after ${timeout} seconds" + exit 1 + fi + echo "PostgreSQL not ready yet... waiting (${counter}/${timeout})" + sleep 1 + done + + echo "✓ PostgreSQL is ready!" +} + +# Function to run database migrations +run_migrations() { + echo "Running database migrations..." + + if npx prisma migrate deploy; then + echo "✓ Migrations completed successfully!" + else + echo "ERROR: Migration failed!" + exit 1 + fi +} + +# Function to seed database (optional) +seed_database() { + if [ "$RUN_SEED" = "true" ]; then + echo "Seeding database..." + + if npx prisma db seed; then + echo "✓ Database seeded successfully!" + else + echo "WARNING: Database seeding failed (continuing anyway)" + fi + else + echo "Skipping database seeding (RUN_SEED not set to 'true')" + fi +} + +# Main execution +main() { + # Wait for database to be available + wait_for_postgres + + # Run migrations + run_migrations + + # Optionally seed database + seed_database + + echo "=== Starting NestJS Application ===" + echo "Node version: $(node --version)" + echo "Environment: ${NODE_ENV:-production}" + echo "Starting server on port 3000..." + + # Start the application + exec node dist/main +} + +# Run main function +main diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..a37caa8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,143 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: vip-coordinator-postgres + environment: + POSTGRES_DB: ${POSTGRES_DB:-vip_coordinator} + POSTGRES_USER: ${POSTGRES_USER:-vip_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set} + volumes: + - vip-coordinator-postgres-data:/var/lib/postgresql/data + networks: + - vip-coordinator-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vip_user}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Redis Cache + redis: + image: redis:7-alpine + container_name: vip-coordinator-redis + volumes: + - vip-coordinator-redis-data:/data + networks: + - vip-coordinator-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + command: redis-server --appendonly yes + + # NestJS Backend API + backend: + build: + context: ./backend + dockerfile: Dockerfile + image: vip-coordinator-backend:latest + container_name: vip-coordinator-backend + environment: + # Database Configuration + DATABASE_URL: postgresql://${POSTGRES_USER:-vip_user}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-vip_coordinator} + + # Redis Configuration + REDIS_URL: redis://redis:6379 + + # Auth0 Configuration + AUTH0_DOMAIN: ${AUTH0_DOMAIN:?AUTH0_DOMAIN must be set} + AUTH0_AUDIENCE: ${AUTH0_AUDIENCE:?AUTH0_AUDIENCE must be set} + AUTH0_ISSUER: ${AUTH0_ISSUER:?AUTH0_ISSUER must be set} + + # Application Configuration + NODE_ENV: production + PORT: 3000 + + # Optional: AviationStack API (for flight tracking) + AVIATIONSTACK_API_KEY: ${AVIATIONSTACK_API_KEY:-} + + # Optional: Database seeding + RUN_SEED: ${RUN_SEED:-false} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - vip-coordinator-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # React Frontend (Nginx) + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + # These are embedded at build time + VITE_API_URL: ${VITE_API_URL:-http://localhost/api/v1} + VITE_AUTH0_DOMAIN: ${AUTH0_DOMAIN:?AUTH0_DOMAIN must be set} + VITE_AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID:?AUTH0_CLIENT_ID must be set} + VITE_AUTH0_AUDIENCE: ${AUTH0_AUDIENCE:?AUTH0_AUDIENCE must be set} + image: vip-coordinator-frontend:latest + container_name: vip-coordinator-frontend + ports: + - "${FRONTEND_PORT:-80}:80" + depends_on: + backend: + condition: service_healthy + networks: + - vip-coordinator-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +# Named volumes for data persistence +volumes: + vip-coordinator-postgres-data: + name: vip-coordinator-postgres-data + vip-coordinator-redis-data: + name: vip-coordinator-redis-data + +# Dedicated network for service communication +networks: + vip-coordinator-network: + name: vip-coordinator-network + driver: bridge diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..b72ac77 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,75 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json + +# Build output +dist +build + +# Environment files (injected at build time via args) +.env +.env.* +!.env.example + +# Testing +e2e +playwright-report +test-results +coverage +*.spec.ts +*.spec.tsx +*.test.ts +*.test.tsx + +# Documentation +*.md +!README.md +docs + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store + +# Git +.git +.gitignore +.gitattributes + +# Logs +logs +*.log + +# Temporary files +tmp +temp +*.tmp +*.temp + +# Docker files (avoid recursion) +Dockerfile* +.dockerignore +docker-compose*.yml + +# CI/CD +.github +.gitlab-ci.yml + +# Development files +public/mockServiceWorker.js + +# Misc +.editorconfig +.eslintrc* +.prettierrc* +tsconfig*.json +vite.config.ts +postcss.config.* +tailwind.config.* +playwright.config.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..cae4650 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,64 @@ +# ========================================== +# Stage 1: Builder +# Build the React application with Vite +# ========================================== +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy application source +COPY . . + +# Accept build-time environment variables +# These are embedded into the build by Vite +ARG VITE_API_URL +ARG VITE_AUTH0_DOMAIN +ARG VITE_AUTH0_CLIENT_ID +ARG VITE_AUTH0_AUDIENCE + +# Set environment variables for build +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_AUTH0_DOMAIN=$VITE_AUTH0_DOMAIN +ENV VITE_AUTH0_CLIENT_ID=$VITE_AUTH0_CLIENT_ID +ENV VITE_AUTH0_AUDIENCE=$VITE_AUTH0_AUDIENCE + +# Build the application +RUN npm run build + +# ========================================== +# Stage 2: Production Runtime +# Serve static files with Nginx +# ========================================== +FROM nginx:1.27-alpine AS production + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built application from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Create non-root user for nginx +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +# Switch to non-root user +USER nginx + +# Expose HTTP port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..0f32b2d --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,83 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/json + application/xml+rss + application/x-font-ttf + font/opentype + image/svg+xml + image/x-icon; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # API proxy - forward all /api requests to backend service + location /api { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Cache static assets with versioned filenames + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Don't cache index.html + location = /index.html { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + } + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Hide nginx version + server_tokens off; + + # Custom error pages + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +}