OPS-011, en partie. Ce que le workflow garantit maintenant : - rien ne part d un arbre casse. La suite, ruff check et ruff format tournent sur le runner de deploiement avant tout envoi. Une CI verte sur GitHub ne prouve rien ici : le deploiement se declenche a la main, sur ce que la branche contient a cet instant ; - seuls les fichiers nommes partent. La charge est une liste blanche — app/, wsgi.py, requirements.txt — et non l arbre de travail moins neuf exclusions. C est par cette porte que clear_db.py, la suite de tests et les definitions de CI se sont retrouves sur le noeud de production ; - le deploiement est verifie. /health est interroge pendant deux minutes apres l envoi et le job echoue s il ne repond jamais « healthy ». Avant, un arbre a moitie televerse etait un deploiement vert. Ce qui n est pas garanti, et c est ecrit dans le fichier : la bascule n est pas atomique. Le miroir se fait sur place, donc pendant le transfert la production execute un melange de deux versions. En cherchant a fermer ce point, un defaut a part entiere est apparu. Les contrats etaient ranges a os.getcwd()/documents et leur chemin absolu ecrit en base. La racine de stockage suivait donc le repertoire depuis lequel le processus avait ete lance : redemarrer le serveur ailleurs envoie les nouveaux contrats dans un nouvel arbre et rend les anciens illisibles — la base continuant d affirmer qu ils sont la, la panne se manifeste par un 500 au telechargement, pas par quelque chose d actionnable. app/storage.py fixe la racine et DOCUMENTS_ROOT la deplace. Les nouvelles lignes gardent un chemin relatif, les anciennes gardent leur chemin absolu et continuent de resoudre : aucune migration de donnees n est necessaire, donc ce changement n attend pas Alembic. C etait aussi le troisieme pre-requis de la bascule par repertoires de version. Les deux autres sont hors d atteinte d ici — la commande de demarrage Pterodactyl doit pointer sur current/, et les repertoires partages doivent etre installes sur le noeud. Les deux sont decrits dans docs/deployment.md, avec la procedure de retour arriere qui manquait. 511 tests.
270 lines
9.6 KiB
Markdown
270 lines
9.6 KiB
Markdown
# Team Tryouts - Production Deployment Guide (Windows)
|
|
|
|
This document outlines the secure production deployment for the Team Tryouts
|
|
Flask application on Windows with Nginx reverse proxy.
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
Internet → Nginx (HTTPS:443) → Waitress (127.0.0.1:5000) → Flask App
|
|
```
|
|
|
|
- **Nginx**: TLS termination, static file serving, rate limiting, security headers
|
|
- **Waitress**: Production WSGI server with multiple worker threads
|
|
- **Flask**: Application logic (never exposed directly to the internet)
|
|
|
|
## Prerequisites
|
|
|
|
1. **Python 3.12+** installed
|
|
2. **Nginx for Windows** downloaded from [nginx.org](https://nginx.org/en/download.html)
|
|
3. **SSL Certificate** (Let's Encrypt via certbot or commercial provider)
|
|
4. **Windows Firewall** configured properly
|
|
|
|
## Step 1: Install Dependencies
|
|
|
|
```powershell
|
|
# Install Python packages
|
|
pip install -r requirements.txt
|
|
|
|
# Create required directories
|
|
mkdir logs
|
|
mkdir backups
|
|
```
|
|
|
|
## Step 2: Configure Environment Variables
|
|
|
|
Create a `.env` file in the project root:
|
|
|
|
```env
|
|
# Security (REQUIRED - generate with: python -c "import secrets; print(secrets.token_hex(32))")
|
|
SECRET_KEY=<your-generated-64-char-hex-key>
|
|
|
|
# Database — PostgreSQL. create_app() refuses to start without this.
|
|
# Either form works; the driver is named for you if you leave it out.
|
|
DATABASE_URL=postgresql://user:password@host:5432/dbname
|
|
|
|
# Security settings
|
|
SESSION_COOKIE_SECURE=true
|
|
FORCE_HTTPS=true
|
|
FLASK_DEBUG=false
|
|
|
|
# CORS (set to your actual domain in production)
|
|
CORS_ALLOWED_ORIGINS=https://yourdomain.com
|
|
|
|
# Discord bot (optional)
|
|
DISCORD_BOT_TOKEN=
|
|
DISCORD_WEBHOOK_URL=
|
|
|
|
# Backup settings
|
|
BACKUP_DIR=./backups
|
|
BACKUP_RETENTION_DAYS=30
|
|
```
|
|
|
|
**Important**: Never commit `.env` to version control.
|
|
|
|
## Step 3: Configure Nginx
|
|
|
|
1. Copy `app/nginx.conf` to your Nginx installation directory (e.g., `C:\nginx\conf\`)
|
|
2. **Edit the `alias` in the `location /static/` block** to point at this
|
|
checkout's `app/static/` directory — absolute path, forward slashes, keep
|
|
the trailing slash. It ships as `C:/team-tryouts/app/static/`, which is a
|
|
guess about your machine. Nginx resolves a relative path against its own
|
|
install prefix, not against `nginx.conf`.
|
|
3. Place SSL certificate files:
|
|
- `C:\nginx\certs\fullchain.pem`
|
|
- `C:\nginx\certs\privkey.pem`
|
|
4. Check the configuration parses before restarting: `C:\nginx\nginx.exe -t`
|
|
5. Start Nginx: `C:\nginx\nginx.exe`
|
|
|
|
Nginx serves `/static/` from disk with a 30-day `immutable` cache. That is
|
|
only safe because `url_for('static', …)` appends `?v=<mtime>` to every static
|
|
URL (`version_static_urls` in `app/app.py`), so a redeployed file is requested
|
|
under a new URL. If that stamp is ever removed, remove the cache headers with
|
|
it or visitors keep a month-old stylesheet.
|
|
|
|
After a deploy, confirm the stamp changed rather than trusting it:
|
|
|
|
```powershell
|
|
# The v= value must differ from the one served before the deploy.
|
|
(Invoke-WebRequest https://your-domain/auth/login).Content -match 'style\.css\?v=(\d+)'
|
|
```
|
|
|
|
### Obtaining SSL Certificates
|
|
|
|
Using Let's Encrypt with certbot (recommended):
|
|
|
|
```powershell
|
|
# Using certbot on Windows
|
|
certbot certonly --standalone -d yourdomain.com
|
|
```
|
|
|
|
Or use your hosting provider's SSL certificate.
|
|
|
|
## Step 4: Start the Application
|
|
|
|
```powershell
|
|
# Production start
|
|
python wsgi.py
|
|
|
|
# Or with custom port/threads
|
|
$env:PORT=5000
|
|
$env:WAITRESS_THREADS=5
|
|
python wsgi.py
|
|
```
|
|
|
|
## Step 5: Configure Windows Firewall
|
|
|
|
```powershell
|
|
# Allow only necessary ports
|
|
New-NetFirewallRule -DisplayName "Nginx HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow
|
|
New-NetFirewallRule -DisplayName "Nginx HTTP Redirect" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow
|
|
|
|
# Block direct access to Waitress port (5000) from outside
|
|
New-NetFirewallRule -DisplayName "Block Waitress External" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Block
|
|
```
|
|
|
|
## Step 6: Set Up Automated Backups
|
|
|
|
Create a scheduled task for daily backups:
|
|
|
|
```powershell
|
|
# Run backup script daily at 2:00 AM
|
|
$Action = New-ScheduledTaskAction -Execute "python" -Argument "backup.py" -WorkingDirectory "C:\path\to\team-tryouts"
|
|
$Trigger = New-ScheduledTaskTrigger -Daily -At 2:00AM
|
|
Register-ScheduledTask -TaskName "TeamTryoutsBackup" -Action $Action -Trigger $Trigger
|
|
```
|
|
|
|
## Step 7: Run Security Validation
|
|
|
|
Before going live:
|
|
|
|
```powershell
|
|
# Run the security scanner
|
|
python security_scan.py --url https://yourdomain.com
|
|
```
|
|
|
|
All checks must pass before deployment.
|
|
|
|
## Deploying, and undoing a deployment (OPS-011)
|
|
|
|
Deployment is the Gitea workflow `.gitea/workflows/git-to-ptero.yaml`, run by
|
|
hand (`workflow_dispatch`). It mirrors files over SFTP to the Pterodactyl
|
|
node.
|
|
|
|
### What the workflow guarantees
|
|
|
|
1. **Nothing ships from a broken tree.** The suite, `ruff check` and
|
|
`ruff format --check` all run on the deploy runner first. A green CI on
|
|
GitHub proves nothing here: the deploy is triggered by hand, on whatever
|
|
the branch currently holds.
|
|
2. **Only named files ship.** The payload is an allowlist — `app/`,
|
|
`wsgi.py`, `requirements.txt` — not the working tree minus exclusions.
|
|
A new file at the repository root does not reach production unless
|
|
somebody adds it. The old form is how `clear_db.py`, the test suite and
|
|
the CI definitions got onto the production node.
|
|
3. **The deployment is verified.** `/health` is polled for two minutes after
|
|
the upload and the job fails if it never reports healthy. Set the
|
|
`HEALTH_URL` repository secret to `https://<host>/health`; without it the
|
|
workflow warns that the deployment went out unverified.
|
|
|
|
Deliberately *not* shipped: `run.py` (development entry point), `tests/`,
|
|
`migrations/add_tryout_coaches.py` (a one-off, already applied — run it by
|
|
hand if a fresh database ever needs it), `docs/`, `audit/`, `clear_db.py`.
|
|
|
|
### What it does not guarantee
|
|
|
|
**The switch is not atomic.** Files are mirrored in place, so for the length
|
|
of the transfer the node runs a mixture of two versions. And because
|
|
`--delete` is off — uploaded contracts, logs and the server's `.env` live
|
|
under the deployment root and are absent from the repository — a file
|
|
removed from the repository stays on the server for ever.
|
|
|
|
### Rolling back
|
|
|
|
There is no previous release on the node to switch back to, so a rollback is
|
|
a forward deployment of a known-good commit:
|
|
|
|
```bash
|
|
# 1. Find the last deployment that was verified healthy — the workflow run
|
|
# log names the commit.
|
|
git log --oneline
|
|
|
|
# 2. Deploy that commit. In the Gitea UI, run the "Push to SFTP" workflow
|
|
# against the tag or branch pointing at it. Tag known-good releases so
|
|
# this step does not depend on reading a log:
|
|
git tag -a deploy-2026-08-11 -m "verified healthy" <commit>
|
|
git push origin deploy-2026-08-11 # requires the push freeze to be lifted
|
|
|
|
# 3. Confirm.
|
|
curl -fsS https://<host>/health
|
|
```
|
|
|
|
A rollback does **not** undo a database migration. If the deployment that
|
|
broke production also changed the schema, restore from backup first —
|
|
`docs/database-restore.md`.
|
|
|
|
### Making the switch atomic
|
|
|
|
The remaining work, and why it is not done here. A release-directory layout
|
|
looks like this on the node:
|
|
|
|
```
|
|
/home/container/
|
|
├── releases/
|
|
│ ├── 2026-08-11-a1b2c3/
|
|
│ └── 2026-08-10-9f8e7d/
|
|
├── current -> releases/2026-08-11-a1b2c3
|
|
├── documents/ # shared, never inside a release
|
|
├── logs/ # shared
|
|
└── .env # shared
|
|
```
|
|
|
|
Three prerequisites. One is done, two are not:
|
|
|
|
| # | Prerequisite | State |
|
|
|---|---|---|
|
|
| 1 | The Pterodactyl startup command must run the app from `current/`, and the server must be restarted on switch | **Panel change.** Cannot be made or verified from the repository |
|
|
| 2 | `documents/`, `logs/` and `.env` must sit beside the releases, not inside one. `DOCUMENTS_ROOT` points the document store at a fixed path (`app/storage.py`) | Mechanism ready, **not yet configured on the node** |
|
|
| 3 | Contract paths must be relative to that root, or the first switch strands every contract ever uploaded | **Done.** New rows store a relative path; rows written earlier keep their absolute one and still resolve, so no data migration is needed |
|
|
|
|
Prerequisite 3 was a defect on its own, not just a blocker: paths were built
|
|
from `os.getcwd()`, so starting the server from a different directory would
|
|
have sent new contracts to a new tree and made the existing ones unreadable
|
|
— with the database still claiming they were there.
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] `.env` is not committed to repository
|
|
- [ ] `SECRET_KEY` is strong (64+ hex characters, randomly generated)
|
|
- [ ] `FLASK_DEBUG` is set to `false`
|
|
- [ ] Nginx is running with HTTPS (port 443)
|
|
- [ ] HTTP (port 80) redirects to HTTPS
|
|
- [ ] TLS 1.2+ configured (TLS 1.0/1.1 disabled)
|
|
- [ ] HSTS header is present (`max-age=31536000; includeSubDomains; preload`)
|
|
- [ ] CSP header is configured with no `unsafe-eval`
|
|
- [ ] `server_tokens` is `off` in Nginx
|
|
- [ ] File uploads restricted to PDF only
|
|
- [ ] File uploads limited to 16MB
|
|
- [ ] Database backups scheduled daily
|
|
- [ ] Firewall rules applied (only 80/443 open)
|
|
- [ ] Application is not running as administrator
|
|
- [ ] Logs directory exists and is writable
|
|
- [ ] Health check endpoint returns 200
|
|
|
|
## Monitoring
|
|
|
|
- **Application logs**: `./logs/app.log`
|
|
- **Error logs**: `./logs/errors.log`
|
|
- **Auth logs**: `./logs/auth.log`
|
|
- **Nginx access logs**: `C:\nginx\logs\access.log`
|
|
- **Nginx error logs**: `C:\nginx\logs\error.log`
|
|
|
|
Monitor these logs regularly for suspicious activity.
|
|
|
|
## Maintenance
|
|
|
|
- Run `pip-audit` weekly to check for vulnerabilities
|
|
- Run `python security_scan.py` after any configuration changes
|
|
- Test backup restoration quarterly
|
|
- Review and rotate `SECRET_KEY` if compromised
|
|
- Keep Python and system packages updated |