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:
- Gitea Actions workflow builds the Docker image on Ubuntu
- Image is pushed to the Gitea container registry
- 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/.envhas strong, unique passwordsNEXTAUTH_SECRETis 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