MOPC-App/docs/architecture/infrastructure.md

18 KiB

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

# 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.

# 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)

# 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

# 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

# /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

# 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)

# 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 <noreply@monaco-opc.com>

Generate Secrets

# Generate NEXTAUTH_SECRET
openssl rand -base64 32

# Generate DB_PASSWORD
openssl rand -base64 24

Deployment Commands

Initial Deployment

# 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

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

# 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

# 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

# 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

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

# 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

# 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

# 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

# Test connection from app container
docker compose exec app sh
nc -zv postgres 5432

# Check PostgreSQL logs
docker compose logs postgres

SSL Certificate Issues

# Test certificate
sudo certbot certificates

# Force renewal
sudo certbot renew --force-renewal

# Check Nginx configuration
sudo nginx -t