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:
318
DEPLOYMENT.md
Normal file
318
DEPLOYMENT.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# 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 <your-repo-url> /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 <your-registry-url>
|
||||
|
||||
# Check image exists
|
||||
docker pull <your-registry-url>/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
|
||||
Reference in New Issue
Block a user