# MOPC Platform - Infrastructure ## Overview The MOPC platform is self-hosted on a Linux VPS at **monaco-opc.com** with the following architecture: - **Nginx** (host-level) - Reverse proxy with SSL termination - **Docker Compose** (MOPC stack) - Next.js + PostgreSQL - **MinIO** (separate stack) - S3-compatible file storage - **Poste.io** (separate stack) - Self-hosted email server **Key Configurations:** - Max file size: 500MB (for video uploads) - SSL via Certbot (Let's Encrypt) ## Architecture Diagram ``` ┌──────────────────────────────────────────────────────────────────────────────┐ │ INTERNET │ └─────────────────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ LINUX VPS │ ├──────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ NGINX (Host Level) │ │ │ │ │ │ │ │ - SSL termination via Certbot │ │ │ │ - Reverse proxy to Docker services │ │ │ │ - Rate limiting │ │ │ │ - Security headers │ │ │ │ │ │ │ │ Ports: 80 (HTTP → HTTPS redirect), 443 (HTTPS) │ │ │ └─────────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌───────────────────────┼───────────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ │ MOPC Stack │ │ MinIO Stack │ │ Poste.io Stack │ │ │ │ (Docker Compose)│ │ (Docker Compose)│ │ (Docker Compose)│ │ │ │ │ │ │ │ │ │ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ │ │ │ Next.js │ │ │ │ MinIO │ │ │ │ Poste.io │ │ │ │ │ │ :3000 │ │ │ │ :9000 │ │ │ │ :25,587 │ │ │ │ │ └────────────┘ │ │ │ :9001 │ │ │ └────────────┘ │ │ │ │ │ │ └────────────┘ │ │ │ │ │ │ ┌────────────┐ │ │ │ │ │ │ │ │ │ PostgreSQL │ │ │ │ │ │ │ │ │ │ :5432 │ │ │ │ │ │ │ │ │ └────────────┘ │ │ │ │ │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │ │ │ Data Volumes: │ │ /data/mopc/postgres /data/minio /data/poste │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ ``` ## Docker Compose Configuration ### MOPC Stack ```yaml # docker/docker-compose.yml version: '3.8' services: app: build: context: .. dockerfile: docker/Dockerfile container_name: mopc-app restart: unless-stopped ports: - "127.0.0.1:3000:3000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc - NEXTAUTH_URL=${NEXTAUTH_URL} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - MINIO_ENDPOINT=${MINIO_ENDPOINT} - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - MINIO_BUCKET=${MINIO_BUCKET} - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_USER=${SMTP_USER} - SMTP_PASS=${SMTP_PASS} - EMAIL_FROM=${EMAIL_FROM} depends_on: postgres: condition: service_healthy networks: - mopc-network postgres: image: postgres:16-alpine container_name: mopc-postgres restart: unless-stopped environment: - POSTGRES_USER=mopc - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=mopc volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U mopc"] interval: 10s timeout: 5s retries: 5 networks: - mopc-network volumes: postgres_data: driver: local driver_opts: type: none o: bind device: /data/mopc/postgres networks: mopc-network: driver: bridge ``` ### Development Stack The development stack includes PostgreSQL, MinIO, and the Next.js app running in Docker containers. ```yaml # docker/docker-compose.dev.yml services: postgres: image: postgres:16-alpine container_name: mopc-postgres-dev ports: - "5432:5432" environment: - POSTGRES_USER=mopc - POSTGRES_PASSWORD=devpassword - POSTGRES_DB=mopc volumes: - postgres_dev_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U mopc"] interval: 5s timeout: 5s retries: 5 minio: image: minio/minio container_name: mopc-minio-dev ports: - "9000:9000" - "9001:9001" environment: - MINIO_ROOT_USER=minioadmin - MINIO_ROOT_PASSWORD=minioadmin volumes: - minio_dev_data:/data command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 # MinIO client to create default bucket on startup createbuckets: image: minio/mc depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set myminio http://minio:9000 minioadmin minioadmin; mc mb --ignore-existing myminio/mopc-files; mc anonymous set download myminio/mopc-files; echo 'Bucket created successfully'; " # Next.js application app: build: context: .. dockerfile: docker/Dockerfile.dev container_name: mopc-app-dev ports: - "3000:3000" environment: - DATABASE_URL=postgresql://mopc:devpassword@postgres:5432/mopc - NEXTAUTH_URL=http://localhost:3000 - NEXTAUTH_SECRET=dev-secret-key-for-local-development-only - MINIO_ENDPOINT=http://minio:9000 - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin - MINIO_BUCKET=mopc-files - NODE_ENV=development volumes: - ../src:/app/src - ../public:/app/public - ../prisma:/app/prisma depends_on: postgres: condition: service_healthy minio: condition: service_healthy volumes: postgres_dev_data: minio_dev_data: ``` ### Quick Start (Development) ```bash # 1. Start all services (PostgreSQL, MinIO, Next.js) docker compose -f docker/docker-compose.dev.yml up --build -d # 2. Push database schema docker exec mopc-app-dev npx prisma db push # 3. Seed test data docker exec mopc-app-dev npx tsx prisma/seed.ts # 4. Open http://localhost:3000 # Login with: admin@monaco-opc.com (magic link) ``` ### Development URLs | Service | URL | Credentials | |---------|-----|-------------| | Next.js App | http://localhost:3000 | See seed data | | MinIO Console | http://localhost:9001 | minioadmin / minioadmin | | PostgreSQL | localhost:5432 | mopc / devpassword | ### Test Accounts (after seeding) | Role | Email | |------|-------| | Super Admin | admin@monaco-opc.com | | Jury Member | jury1@example.com | | Jury Member | jury2@example.com | | Jury Member | jury3@example.com | ## Dockerfile ```dockerfile # docker/Dockerfile FROM node:20-alpine AS base # Install dependencies only when needed FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Generate Prisma client RUN npx prisma generate # Build Next.js ENV NEXT_TELEMETRY_DISABLED 1 RUN npm run build # Production image FROM base 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 COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/prisma ./prisma USER nextjs EXPOSE 3000 ENV PORT 3000 ENV HOSTNAME "0.0.0.0" CMD ["node", "server.js"] ``` ## Nginx Configuration ```nginx # /etc/nginx/sites-available/mopc-platform # Rate limiting zone limit_req_zone $binary_remote_addr zone=mopc_limit:10m rate=10r/s; # MOPC Platform server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name monaco-opc.com; # SSL certificates (managed by Certbot) ssl_certificate /etc/letsencrypt/live/monaco-opc.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/monaco-opc.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # 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 "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self';" always; # File upload size (500MB for videos) client_max_body_size 500M; # Rate limiting limit_req zone=mopc_limit burst=20 nodelay; # Logging access_log /var/log/nginx/mopc-access.log; error_log /var/log/nginx/mopc-error.log; # Next.js application location / { proxy_pass http://127.0.0.1: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 for large file uploads proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; } # Static files caching location /_next/static { proxy_pass http://127.0.0.1:3000; proxy_cache_valid 200 365d; add_header Cache-Control "public, max-age=31536000, immutable"; } # Health check endpoint location /api/health { proxy_pass http://127.0.0.1:3000; access_log off; } } # HTTP to HTTPS redirect server { listen 80; listen [::]:80; server_name monaco-opc.com; return 301 https://$host$request_uri; } ``` ## SSL Setup with Certbot ```bash # Install Certbot sudo apt update sudo apt install certbot python3-certbot-nginx # Obtain certificate sudo certbot --nginx -d monaco-opc.com # Auto-renewal is configured automatically # Test renewal sudo certbot renew --dry-run ``` ## Environment Variables ### Production (.env) ```env # Application NODE_ENV=production NEXTAUTH_URL=https://monaco-opc.com NEXTAUTH_SECRET=generate-a-secure-random-string-here # Database DB_PASSWORD=your-secure-database-password DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc # MinIO (external stack) MINIO_ENDPOINT=http://localhost:9000 MINIO_ACCESS_KEY=your-minio-access-key MINIO_SECRET_KEY=your-minio-secret-key MINIO_BUCKET=mopc-files # Email (Poste.io) SMTP_HOST=localhost SMTP_PORT=587 SMTP_USER=noreply@monaco-opc.com SMTP_PASS=your-smtp-password EMAIL_FROM=MOPC Platform ``` ### Generate Secrets ```bash # Generate NEXTAUTH_SECRET openssl rand -base64 32 # Generate DB_PASSWORD openssl rand -base64 24 ``` ## Deployment Commands ### Initial Deployment ```bash # 1. Clone repository git clone https://github.com/your-org/mopc-platform.git /opt/mopc cd /opt/mopc # 2. Create environment file cp .env.example .env nano .env # Edit with production values # 3. Create data directories sudo mkdir -p /data/mopc/postgres sudo chown -R 1000:1000 /data/mopc # 4. Start the stack cd docker docker compose up -d # 5. Run database migrations docker compose exec app npx prisma migrate deploy # 6. Seed initial data (optional) docker compose exec app npx prisma db seed # 7. Enable Nginx site sudo ln -s /etc/nginx/sites-available/mopc-platform /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx # 8. Set up SSL sudo certbot --nginx -d monaco-opc.com ``` ### Updates ```bash cd /opt/mopc # 1. Pull latest code git pull origin main # 2. Rebuild and restart cd docker docker compose build app docker compose up -d app # 3. Run any new migrations docker compose exec app npx prisma migrate deploy ``` ### Rollback ```bash # Revert to previous image docker compose down git checkout HEAD~1 docker compose build app docker compose up -d # Or restore from specific tag git checkout v1.0.0 docker compose build app docker compose up -d ``` ## Backup Strategy ### Database Backups ```bash # Create backup script cat > /opt/mopc/scripts/backup-db.sh << 'EOF' #!/bin/bash BACKUP_DIR=/data/backups/mopc DATE=$(date +%Y%m%d_%H%M%S) BACKUP_FILE=$BACKUP_DIR/mopc_$DATE.sql.gz mkdir -p $BACKUP_DIR docker exec mopc-postgres pg_dump -U mopc mopc | gzip > $BACKUP_FILE # Keep last 30 days find $BACKUP_DIR -name "mopc_*.sql.gz" -mtime +30 -delete echo "Backup completed: $BACKUP_FILE" EOF chmod +x /opt/mopc/scripts/backup-db.sh # Add to crontab (daily at 2 AM) echo "0 2 * * * /opt/mopc/scripts/backup-db.sh >> /var/log/mopc-backup.log 2>&1" | sudo tee -a /etc/cron.d/mopc-backup ``` ### Restore Database ```bash # Restore from backup gunzip < /data/backups/mopc/mopc_20260115_020000.sql.gz | docker exec -i mopc-postgres psql -U mopc mopc ``` ## Monitoring ### Health Check Endpoint ```typescript // src/app/api/health/route.ts import { prisma } from '@/lib/prisma' export async function GET() { try { // Check database connection await prisma.$queryRaw`SELECT 1` return Response.json({ status: 'healthy', timestamp: new Date().toISOString(), }) } catch (error) { return Response.json( { status: 'unhealthy', error: 'Database connection failed', }, { status: 503 } ) } } ``` ### Log Viewing ```bash # Application logs docker compose logs -f app # Nginx access logs tail -f /var/log/nginx/mopc-access.log # Nginx error logs tail -f /var/log/nginx/mopc-error.log # PostgreSQL logs docker compose logs -f postgres ``` ### Resource Monitoring ```bash # Docker stats docker stats mopc-app mopc-postgres # System resources htop ``` ## Security Checklist - [ ] SSL certificate active and auto-renewing - [ ] Database password is strong and unique - [ ] NEXTAUTH_SECRET is randomly generated - [ ] MinIO credentials are secure - [ ] SMTP credentials are secure - [ ] Firewall allows only ports 80, 443, 22 - [ ] Docker daemon not exposed to network - [ ] Regular backups configured - [ ] Log rotation configured - [ ] Security headers enabled in Nginx ## Troubleshooting ### Application Won't Start ```bash # Check logs docker compose logs app # Check if database is ready docker compose exec postgres pg_isready -U mopc # Restart stack docker compose restart ``` ### Database Connection Issues ```bash # Test connection from app container docker compose exec app sh nc -zv postgres 5432 # Check PostgreSQL logs docker compose logs postgres ``` ### SSL Certificate Issues ```bash # Test certificate sudo certbot certificates # Force renewal sudo certbot renew --force-renewal # Check Nginx configuration sudo nginx -t ``` ## Related Documentation - [Database Design](./database.md) - Schema and migrations - [API Design](./api.md) - tRPC endpoints