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.
9.6 KiB
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
- Python 3.12+ installed
- Nginx for Windows downloaded from nginx.org
- SSL Certificate (Let's Encrypt via certbot or commercial provider)
- Windows Firewall configured properly
Step 1: Install Dependencies
# 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:
# 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
- Copy
app/nginx.confto your Nginx installation directory (e.g.,C:\nginx\conf\) - Edit the
aliasin thelocation /static/block to point at this checkout'sapp/static/directory — absolute path, forward slashes, keep the trailing slash. It ships asC:/team-tryouts/app/static/, which is a guess about your machine. Nginx resolves a relative path against its own install prefix, not againstnginx.conf. - Place SSL certificate files:
C:\nginx\certs\fullchain.pemC:\nginx\certs\privkey.pem
- Check the configuration parses before restarting:
C:\nginx\nginx.exe -t - 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:
# 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):
# Using certbot on Windows
certbot certonly --standalone -d yourdomain.com
Or use your hosting provider's SSL certificate.
Step 4: Start the Application
# 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
# 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:
# 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:
# 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
- Nothing ships from a broken tree. The suite,
ruff checkandruff format --checkall 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. - 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 howclear_db.py, the test suite and the CI definitions got onto the production node. - The deployment is verified.
/healthis polled for two minutes after the upload and the job fails if it never reports healthy. Set theHEALTH_URLrepository secret tohttps://<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:
# 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
.envis not committed to repositorySECRET_KEYis strong (64+ hex characters, randomly generated)FLASK_DEBUGis set tofalse- 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_tokensisoffin 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-auditweekly to check for vulnerabilities - Run
python security_scan.pyafter any configuration changes - Test backup restoration quarterly
- Review and rotate
SECRET_KEYif compromised - Keep Python and system packages updated