# MOPC Platform - Server Deployment Guide Deployment guide for the MOPC platform on a Linux VPS with Docker. **Domain**: `portal.monaco-opc.com` **App Port**: 7600 (behind Nginx reverse proxy) **CI/CD**: Gitea Actions (Ubuntu runner) builds and pushes Docker images ## CI/CD Pipeline The app is built automatically by a Gitea runner on every push to `main`: 1. Gitea Actions workflow builds the Docker image on Ubuntu 2. Image is pushed to the Gitea container registry 3. On the server, you pull the latest image and restart ### Gitea Setup Configure the following in your Gitea repository settings: **Repository Variables** (Settings > Actions > Variables): | Variable | Value | |----------|-------| | `REGISTRY_URL` | Your Gitea registry URL (e.g. `gitea.example.com/your-org`) | **Repository Secrets** (Settings > Actions > Secrets): | Secret | Value | |--------|-------| | `REGISTRY_USER` | Gitea username with registry access | | `REGISTRY_PASSWORD` | Gitea access token or password | The workflow file is at `.gitea/workflows/build.yml`. ## Prerequisites - Linux VPS (Ubuntu 22.04+ recommended) - Docker Engine 24+ with Compose v2 - Nginx installed on the host - Certbot for SSL certificates ### Install Docker (if needed) ```bash curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # Log out and back in ``` ### Install Nginx & Certbot (if needed) ```bash sudo apt update sudo apt install nginx certbot python3-certbot-nginx ``` ## First-Time Deployment ### 1. Clone the repository ```bash git clone /opt/mopc cd /opt/mopc ``` ### 2. Configure environment variables ```bash cp docker/.env.production docker/.env nano docker/.env ``` Fill in all `CHANGE_ME` values. Generate secrets with: ```bash openssl rand -base64 32 ``` Required variables: | Variable | Description | |----------|-------------| | `REGISTRY_URL` | Gitea registry URL (e.g. `gitea.example.com/your-org`) | | `DB_PASSWORD` | PostgreSQL password | | `NEXTAUTH_SECRET` | Auth session secret (openssl rand) | | `NEXTAUTH_URL` | `https://portal.monaco-opc.com` | | `MINIO_ENDPOINT` | MinIO internal URL (e.g. `http://localhost:9000`) | | `MINIO_ACCESS_KEY` | MinIO access key | | `MINIO_SECRET_KEY` | MinIO secret key | | `MINIO_BUCKET` | MinIO bucket name (`mopc-files`) | | `SMTP_HOST` | SMTP server host | | `SMTP_PORT` | SMTP port (587) | | `SMTP_USER` | SMTP username | | `SMTP_PASS` | SMTP password | | `EMAIL_FROM` | Sender address | ### 3. Run the deploy script ```bash chmod +x scripts/deploy.sh scripts/seed.sh scripts/update.sh ./scripts/deploy.sh ``` This will: - Log in to the container registry - Pull the latest app image - Start PostgreSQL + the app - Run database migrations automatically on startup - Wait for the health check ### 4. Seed the database (one time only) ```bash ./scripts/seed.sh ``` This seeds: - Super admin user (`matt.ciaccio@gmail.com`) - System settings - Program & Round 1 configuration - Evaluation form - All candidature data from CSV ### 5. Set up Nginx ```bash sudo ln -s /opt/mopc/docker/nginx/mopc-platform.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` ### 6. Set up SSL ```bash sudo certbot --nginx -d portal.monaco-opc.com ``` Auto-renewal is configured by default. Test with: ```bash sudo certbot renew --dry-run ``` ### 7. Verify ```bash curl https://portal.monaco-opc.com/api/health ``` Expected response: ```json {"status":"healthy","timestamp":"...","services":{"database":"connected"}} ``` ## Updating the Platform After Gitea CI builds a new image (push to `main`): ```bash cd /opt/mopc ./scripts/update.sh ``` This pulls the latest image from the registry, restarts only the app container (PostgreSQL stays running), runs migrations via the entrypoint, and waits for the health check. ## Manual Operations ### View logs ```bash cd /opt/mopc/docker docker compose logs -f app # App logs docker compose logs -f postgres # Database logs ``` ### Run migrations manually ```bash cd /opt/mopc/docker docker compose exec app npx prisma migrate deploy ``` ### Open a shell in the app container ```bash cd /opt/mopc/docker docker compose exec app sh ``` ### Restart services ```bash cd /opt/mopc/docker docker compose restart app # App only docker compose restart # All services ``` ### Stop everything ```bash cd /opt/mopc/docker docker compose down # Stop containers (data preserved) docker compose down -v # Stop AND delete volumes (data lost!) ``` ## Database Backups ### Create a backup ```bash docker exec mopc-postgres pg_dump -U mopc mopc | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz ``` ### Restore a backup ```bash gunzip < backup_20260130_020000.sql.gz | docker exec -i mopc-postgres psql -U mopc mopc ``` ### Set up daily backups (cron) ```bash sudo mkdir -p /data/backups/mopc cat > /opt/mopc/scripts/backup-db.sh << 'SCRIPT' #!/bin/bash BACKUP_DIR=/data/backups/mopc DATE=$(date +%Y%m%d_%H%M%S) docker exec mopc-postgres pg_dump -U mopc mopc | gzip > $BACKUP_DIR/mopc_$DATE.sql.gz find $BACKUP_DIR -name "mopc_*.sql.gz" -mtime +30 -delete SCRIPT chmod +x /opt/mopc/scripts/backup-db.sh echo "0 2 * * * /opt/mopc/scripts/backup-db.sh >> /var/log/mopc-backup.log 2>&1" | sudo tee /etc/cron.d/mopc-backup ``` ## Architecture ``` Gitea CI (Ubuntu runner) | v (docker push) Container Registry | v (docker pull) Linux VPS | v Nginx (host, port 443) -- SSL termination | v mopc-app (Docker, port 7600) -- Next.js standalone | v mopc-postgres (Docker, port 5432) -- PostgreSQL 16 External services (separate Docker stacks): - MinIO (port 9000) -- S3-compatible file storage - Poste.io (port 587) -- SMTP email ``` ## Troubleshooting ### App won't start ```bash cd /opt/mopc/docker docker compose logs app docker compose exec postgres pg_isready -U mopc ``` ### Can't pull image ```bash # Re-authenticate with registry docker login # Check image exists docker pull /mopc-app:latest ``` ### Migration fails ```bash # Check migration status docker compose exec app npx prisma migrate status # Reset (DESTROYS DATA): docker compose exec app npx prisma migrate reset ``` ### SSL certificate issues ```bash sudo certbot certificates sudo certbot renew --force-renewal ``` ### Port conflict The app runs on port 7600. If something else uses it: ```bash sudo ss -tlnp | grep 7600 ``` ## Security Checklist - [ ] SSL certificate active and auto-renewing - [ ] `docker/.env` has strong, unique passwords - [ ] `NEXTAUTH_SECRET` is randomly generated - [ ] Gitea registry credentials secured - [ ] Firewall allows only ports 80, 443, 22 - [ ] Docker daemon not exposed to network - [ ] Daily backups configured - [ ] Nginx security headers active