MOPC-App/DEPLOYMENT.md

6.7 KiB

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)

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in

Install Nginx & Certbot (if needed)

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

First-Time Deployment

1. Clone the repository

git clone <your-repo-url> /opt/mopc
cd /opt/mopc

2. Configure environment variables

cp docker/.env.production docker/.env
nano docker/.env

Fill in all CHANGE_ME values. Generate secrets with:

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

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)

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

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

sudo certbot --nginx -d portal.monaco-opc.com

Auto-renewal is configured by default. Test with:

sudo certbot renew --dry-run

7. Verify

curl https://portal.monaco-opc.com/api/health

Expected response:

{"status":"healthy","timestamp":"...","services":{"database":"connected"}}

Updating the Platform

After Gitea CI builds a new image (push to main):

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

cd /opt/mopc/docker
docker compose logs -f app       # App logs
docker compose logs -f postgres   # Database logs

Run migrations manually

cd /opt/mopc/docker
docker compose exec app npx prisma migrate deploy

Open a shell in the app container

cd /opt/mopc/docker
docker compose exec app sh

Restart services

cd /opt/mopc/docker
docker compose restart app       # App only
docker compose restart           # All services

Stop everything

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

docker exec mopc-postgres pg_dump -U mopc mopc | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz

Restore a backup

gunzip < backup_20260130_020000.sql.gz | docker exec -i mopc-postgres psql -U mopc mopc

Set up daily backups (cron)

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

cd /opt/mopc/docker
docker compose logs app
docker compose exec postgres pg_isready -U mopc

Can't pull image

# Re-authenticate with registry
docker login <your-registry-url>

# Check image exists
docker pull <your-registry-url>/mopc-app:latest

Migration fails

# 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

sudo certbot certificates
sudo certbot renew --force-renewal

Port conflict

The app runs on port 7600. If something else uses it:

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