7.2 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,
docker compose up -drefreshes the image and restarts the app
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 equivalent:
cd /opt/mopc/docker
docker compose up -d --pull always --force-recreate app
prisma migrate deploy runs automatically in the container entrypoint before the app starts.
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