Initial commit: MOPC platform with Docker deployment setup

Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth.
Includes production Dockerfile (multi-stage, port 7600), docker-compose
with registry-based image pull, Gitea Actions CI workflow, nginx config
for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:41:32 +01:00
commit a606292aaa
290 changed files with 70691 additions and 0 deletions

60
docker/.env.production Normal file
View File

@@ -0,0 +1,60 @@
# =============================================================================
# MOPC Platform - Production Environment Variables
# =============================================================================
# Copy this file to docker/.env and fill in real values:
# cp docker/.env.production docker/.env
#
# Generate secrets with:
# openssl rand -base64 32
# =============================================================================
# DATABASE
# =============================================================================
DB_PASSWORD=CHANGE_ME_use_openssl_rand
# =============================================================================
# AUTHENTICATION (NextAuth.js / Auth.js)
# =============================================================================
NEXTAUTH_URL=https://portal.monaco-opc.com
NEXTAUTH_SECRET=CHANGE_ME_use_openssl_rand
# =============================================================================
# FILE STORAGE (MinIO - external stack)
# =============================================================================
# Internal endpoint (server-to-server, within Docker host)
MINIO_ENDPOINT=http://localhost:9000
# Public endpoint for browser-accessible pre-signed URLs
# Set this when MinIO is behind a reverse proxy
# MINIO_PUBLIC_ENDPOINT=https://storage.monaco-opc.com
MINIO_ACCESS_KEY=CHANGE_ME
MINIO_SECRET_KEY=CHANGE_ME
MINIO_BUCKET=mopc-files
# =============================================================================
# EMAIL (SMTP via Poste.io - external stack)
# =============================================================================
SMTP_HOST=localhost
SMTP_PORT=587
SMTP_USER=noreply@monaco-opc.com
SMTP_PASS=CHANGE_ME
EMAIL_FROM=MOPC Platform <noreply@monaco-opc.com>
# =============================================================================
# AI (OpenAI - optional)
# =============================================================================
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o
# =============================================================================
# DOCKER REGISTRY (Gitea container registry)
# =============================================================================
# The Gitea registry URL where the CI pushes built images
# Example: gitea.example.com/your-org
REGISTRY_URL=code.letsbe.solutions/letsbe
# =============================================================================
# APPLICATION
# =============================================================================
MAX_FILE_SIZE=524288000

71
docker/Dockerfile Normal file
View File

@@ -0,0 +1,71 @@
# =============================================================================
# MOPC Platform - Production Dockerfile
# =============================================================================
# Multi-stage build for optimized production image
FROM node:22-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
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, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Install runtime dependencies for migrations and seeding
RUN apk add --no-cache libc6-compat
# Copy built files
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
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=builder /app/node_modules/prisma ./node_modules/prisma
# Copy CSV data file for manual seeding
COPY --from=builder /app/docs/candidatures_2026.csv ./docs/candidatures_2026.csv
# Copy entrypoint script
COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh
RUN chmod +x /app/docker-entrypoint.sh
# Set correct permissions
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 7600
ENV PORT=7600
ENV HOSTNAME="0.0.0.0"
# Run via entrypoint (migrate then start)
CMD ["/app/docker-entrypoint.sh"]

34
docker/Dockerfile.dev Normal file
View File

@@ -0,0 +1,34 @@
# =============================================================================
# MOPC Platform - Development Dockerfile (Pre-built)
# =============================================================================
FROM node:22-alpine
WORKDIR /app
# Install dependencies for Prisma and development
RUN apk add --no-cache libc6-compat openssl
# Copy package files
COPY package.json package-lock.json* ./
# Install dependencies
RUN npm install && npm install tailwindcss-animate
# Copy prisma schema for generation
COPY prisma ./prisma
# Generate Prisma client
RUN npx prisma generate
# Copy the rest of the application
COPY . .
# Build the application at build time (pre-compile everything)
RUN npm run build
# Expose port
EXPOSE 3000
# Start production server (uses pre-built files)
CMD ["npm", "start"]

View File

@@ -0,0 +1,97 @@
# =============================================================================
# MOPC Platform - Development Docker Compose
# =============================================================================
# Use this for local development. Includes PostgreSQL, MinIO, and the app.
services:
postgres:
image: postgres:16-alpine
container_name: mopc-postgres-dev
ports:
- "5432:5432"
environment:
- POSTGRES_USER=${POSTGRES_USER:-mopc}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-devpassword}
- POSTGRES_DB=${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=${MINIO_ACCESS_KEY:-minioadmin}
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY:-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
environment:
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-minioadmin}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-minioadmin}
- MINIO_BUCKET=${MINIO_BUCKET:-mopc-files}
entrypoint: >
/bin/sh -c "
mc alias set myminio http://minio:9000 $${MINIO_ACCESS_KEY} $${MINIO_SECRET_KEY};
mc mb --ignore-existing myminio/$${MINIO_BUCKET};
mc anonymous set download myminio/$${MINIO_BUCKET};
echo 'Bucket created successfully';
"
# Next.js application
app:
build:
context: ..
dockerfile: docker/Dockerfile.dev
container_name: mopc-app-dev
ports:
- "3000:3000"
env_file:
- ../.env
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-mopc}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-mopc}
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-dev-secret-key-for-local-development-only}
- AUTH_SECRET=${AUTH_SECRET:-dev-secret-key-for-local-development-only}
- MINIO_ENDPOINT=http://minio:9000
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-minioadmin}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-minioadmin}
- MINIO_BUCKET=${MINIO_BUCKET:-mopc-files}
- NODE_ENV=development
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_USER=${SMTP_USER}
- SMTP_PASS=${SMTP_PASS}
- EMAIL_FROM=${EMAIL_FROM}
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:

75
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,75 @@
# =============================================================================
# MOPC Platform - Production Docker Compose
# =============================================================================
# This stack contains only the Next.js app and PostgreSQL.
# MinIO and Poste.io are external services connected via environment variables.
#
# The app image is built by Gitea CI and pushed to the container registry.
# To pull the latest image: docker compose pull app
# To deploy: docker compose up -d
services:
app:
image: ${REGISTRY_URL}/mopc-app:latest
container_name: mopc-app
restart: unless-stopped
ports:
- "127.0.0.1:7600:7600"
env_file:
- .env
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc
- NEXTAUTH_URL=${NEXTAUTH_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- AUTH_SECRET=${NEXTAUTH_SECRET}
- MINIO_ENDPOINT=${MINIO_ENDPOINT}
- MINIO_PUBLIC_ENDPOINT=${MINIO_PUBLIC_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}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o}
- MAX_FILE_SIZE=${MAX_FILE_SIZE:-524288000}
depends_on:
postgres:
condition: service_healthy
networks:
- mopc-network
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:7600/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
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
networks:
mopc-network:
driver: bridge

View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
echo "==> Running database migrations..."
npx prisma migrate deploy
echo "==> Starting application..."
exec node server.js

View File

@@ -0,0 +1,78 @@
# =============================================================================
# MOPC Platform - Nginx Reverse Proxy Configuration
# =============================================================================
# Install: sudo ln -s /opt/mopc/docker/nginx/mopc-platform.conf /etc/nginx/sites-enabled/
# Test: sudo nginx -t
# Reload: sudo systemctl reload nginx
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=mopc_limit:10m rate=10r/s;
# MOPC Platform - HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name portal.monaco-opc.com;
# SSL certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/portal.monaco-opc.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/portal.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:7600;
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:7600;
proxy_cache_valid 200 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Health check endpoint (no access log noise)
location /api/health {
proxy_pass http://127.0.0.1:7600;
access_log off;
}
}
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name portal.monaco-opc.com;
return 301 https://$host$request_uri;
}