docs(deployment): riscrive le sezioni con percorsi morti sul layout reale (#35)

Sostituito il vecchio monorepo a due cartelle (server/, client/,
requirements.txt, moduli main:app senza namespace) con uv e il layout
attuale (src/backend, src/frontend/flask_app, tre Dockerfile alla radice).

Database Setup: niente più script Python per l'admin — punta alla pagina
/api/setup e ne descrive la disattivazione dopo il primo avvio. Rimossa
la password d'esempio cablata e il comando che stampava database_url
(stampava anche le credenziali).

Docker Deployment: docker compose v2 (non più il trattino), i tre
Dockerfile reali, submodule vendor/visionsuite e VISION_ENGINE_VERSION
obbligatoria alla build. Recommended Architecture/SSL: due percorsi
reali (Traefik con TLS automatico in produzione, Nginx in HTTP semplice
per l'on-premise su LAN), non più un nginx generico con certificato a
mano. Tabella env aggiornata con le variabili introdotte dalla revisione
del 21/09/2026 (APP_DOMAIN, TRUSTED_PROXY_COUNT, WEB_CONCURRENCY,
SESSION_COOKIE_SECURE, VISION_RUN_TIMEOUT_S, VISION_MAX_CONCURRENCY).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:45:55 +02:00
parent 3e99b64fc4
commit ac11cec276
+161 -287
View File
@@ -142,6 +142,18 @@ SSL_KEYFILE=
| `SSL_CERTFILE` | string | (empty) | Path to SSL certificate (production) | | `SSL_CERTFILE` | string | (empty) | Path to SSL certificate (production) |
| `SSL_KEYFILE` | string | (empty) | Path to SSL private key (production) | | `SSL_KEYFILE` | string | (empty) | Path to SSL private key (production) |
Introdotte dalla revisione del 21/09/2026:
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `APP_DOMAIN` | string | *(nessuno, obbligatoria)* | Dominio pubblico dell'installazione, usato nelle rule Traefik di `docker-compose.yml` (solo produzione) |
| `TRUSTED_PROXY_COUNT` | int | 1 | Quanti hop di reverse proxy fidati stanno davanti al server, per calcolare l'IP reale del tablet |
| `WEB_CONCURRENCY` | int | 1 | Numero di worker Uvicorn del server; il `Dockerfile` ne avvia 4 di default indipendentemente da questa variabile |
| `SESSION_COOKIE_SECURE` | bool | true | Cookie di sessione Flask con flag Secure (richiede HTTPS). `false` per le installazioni on-premise su LAN in HTTP semplice (`docker-compose.dev.yml`) |
| `VISION_RUN_TIMEOUT_S` | int | 100 | Timeout, in secondi, di un'esecuzione del worker di visione |
| `VISION_MAX_CONCURRENCY` | int | 2 | Esecuzioni di visione in parallelo accettate dal worker |
| `VISION_ENGINE_VERSION` | string | *(nessuno, obbligatoria alla build)* | Commit di `vendor/visionsuite` stampato nell'immagine `vision` al momento della build — vedi [Docker Deployment](#docker-deployment) |
--- ---
## Database Setup ## Database Setup
@@ -169,63 +181,33 @@ EXIT;
### 2. Run Database Migrations ### 2. Run Database Migrations
```bash The monorepo is gestito con `uv`, non con `requirements.txt` per sottocartella: si esegue dalla radice del repository, con il flag `-c` perché `alembic.ini` vive in `src/backend/migrations/`.
cd server
pip install -r requirements.txt
alembic upgrade head
```
This creates all required tables:
- `users` - System users
- `recipes` - Measurement recipes
- `recipe_versions` - Immutable recipe versions
- `recipe_tasks` - Tasks within recipes
- `recipe_subtasks` - Subtasks within tasks (individual measurements)
- `measurements` - Recorded measurements
- `access_logs` - API access audit trail
- `system_settings` - Configuration key-value pairs
- `recipe_version_audit` - Recipe change history
### 3. Create Initial Admin User (Optional)
Use the Flask client to create the first admin user, or run:
```bash ```bash
cd server uv run alembic -c src/backend/migrations/alembic.ini upgrade head
python -c "
from database import init_db, SessionLocal
from services.auth_service import create_user
import asyncio
async def init():
await init_db()
async with SessionLocal() as db:
await create_user(
db,
username='admin',
password='change_me_first_login',
display_name='Admin',
email='admin@example.com',
roles=['Maker', 'MeasurementTec', 'Metrologist'],
is_admin=True
)
await db.commit()
print('Admin user created: admin / change_me_first_login')
asyncio.run(init())
"
``` ```
Questo è lo stesso comando che il container del server esegue da solo a ogni avvio (`CMD` del `Dockerfile`, prima di `uvicorn`): un deployment Docker non richiede questo passo a mano. La prima migrazione crea lo schema base anche su un database completamente vuoto — non serve più creare le tabelle a parte prima di lanciare `upgrade head`.
### 3. Create Initial Admin User
Non c'è più uno script Python da lanciare a mano: il primo utente amministratore si crea dalla pagina `/api/setup` (protetta da `SETUP_PASSWORD`, vedi sopra), che inizializza il database, crea l'admin con le credenziali fornite nel form e, opzionalmente, carica i dati demo — vedi [`README.md`](../README.md#setup-iniziale).
Una volta creato il primo amministratore, l'endpoint di setup si disattiva da solo: le richieste successive di inizializzazione o creazione utenti ricevono `403 Forbidden`, anche con `SETUP_PASSWORD` corretta. Per rieseguirlo — ad esempio per riconfigurare una stazione da zero — si imposta `SETUP_ALLOW_AFTER_INIT=true` nell'ambiente del server, si esegue l'operazione, e si toglie la variabile: non è pensata per restare attiva in modo permanente.
--- ---
## Server Deployment ## Server Deployment
Docker Compose (sezione successiva) è il metodo raccomandato e quello che gira
in produzione. Questa sezione descrive l'avvio **senza Docker**, utile per
sviluppare sul runner senza ricostruire un'immagine a ogni modifica.
### Development Server ### Development Server
```bash ```bash
cd server uv sync --extra server --extra dev
pip install -r requirements.txt uv run uvicorn src.backend.main:app --reload --host 0.0.0.0 --port 8000
uvicorn main:app --reload --host 0.0.0.0 --port 8000
``` ```
Runs at `http://0.0.0.0:8000` Runs at `http://0.0.0.0:8000`
@@ -234,40 +216,15 @@ API Documentation:
- Swagger UI: `http://localhost:8000/docs` - Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc` - ReDoc: `http://localhost:8000/redoc`
### Production Server (Gunicorn + Uvicorn) ### Production, senza Docker
Install Gunicorn: Lo stesso comando che gira dentro il container (`Dockerfile`), lanciato dalla
radice del repository con l'interprete e il venv pinnati da `uv`:
```bash ```bash
pip install gunicorn uv run uvicorn src.backend.main:app \
``` --host 0.0.0.0 --port 8000 --workers ${WEB_CONCURRENCY:-4} \
--proxy-headers --forwarded-allow-ips='*'
Start server:
```bash
cd server
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--access-logfile /var/log/tiemeasureflow/access.log \
--error-logfile /var/log/tiemeasureflow/error.log \
--log-level info
```
### Production Server (Waitress - Windows)
Install Waitress:
```bash
pip install waitress
```
Start server:
```bash
cd server
waitress-serve --host=0.0.0.0 --port=8000 main:app
``` ```
### systemd Service (Linux) ### systemd Service (Linux)
@@ -280,14 +237,14 @@ Description=TieMeasureFlow API Server
After=network.target mysql.service After=network.target mysql.service
[Service] [Service]
Type=notify Type=simple
User=tiemeasureflow User=tiemeasureflow
WorkingDirectory=/opt/tiemeasureflow/server WorkingDirectory=/opt/tiemeasureflow
Environment="PATH=/opt/tiemeasureflow/venv/bin" EnvironmentFile=/opt/tiemeasureflow/.env
ExecStart=/opt/tiemeasureflow/venv/bin/gunicorn main:app \ Environment="WEB_CONCURRENCY=4"
--workers 4 \ ExecStart=/opt/tiemeasureflow/.venv/bin/uv run uvicorn src.backend.main:app \
--worker-class uvicorn.workers.UvicornWorker \ --host 0.0.0.0 --port 8000 --workers ${WEB_CONCURRENCY} \
--bind 0.0.0.0:8000 --proxy-headers --forwarded-allow-ips='*'
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10
@@ -315,12 +272,14 @@ sudo journalctl -u tiemeasureflow-api -f
## Client Deployment ## Client Deployment
Anche qui, Docker Compose è il metodo raccomandato; questa sezione è per lo
sviluppo o un'installazione manuale senza container.
### Development Server ### Development Server
```bash ```bash
cd client uv sync --extra client --extra dev
pip install -r requirements.txt cd src/frontend/flask_app && uv run --project ../../.. flask run --host 0.0.0.0 --port 5000
flask run --host 0.0.0.0 --port 5000
``` ```
Runs at `http://0.0.0.0:5000` Runs at `http://0.0.0.0:5000`
@@ -328,16 +287,20 @@ Runs at `http://0.0.0.0:5000`
### Compile Tailwind CSS (Optional) ### Compile Tailwind CSS (Optional)
```bash ```bash
cd client cd src/frontend/flask_app
npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch
``` ```
### Production Server (Gunicorn) ### Production, senza Docker
Lo stesso comando del `Dockerfile.frontend` (gunicorn richiede la cwd dentro
`flask_app` per risolvere `app:create_app()`):
```bash ```bash
pip install gunicorn cd src/frontend/flask_app && uv run --project ../../.. gunicorn \
cd client --workers 5 --threads 4 --worker-class gthread --timeout 60 \
gunicorn --workers 4 --bind 0.0.0.0:5000 app:app --bind 0.0.0.0:5000 --forwarded-allow-ips='*' \
"app:create_app()"
``` ```
### systemd Service (Linux) ### systemd Service (Linux)
@@ -350,15 +313,14 @@ Description=TieMeasureFlow Web Client
After=network.target tiemeasureflow-api.service After=network.target tiemeasureflow-api.service
[Service] [Service]
Type=notify Type=simple
User=tiemeasureflow User=tiemeasureflow
WorkingDirectory=/opt/tiemeasureflow/client WorkingDirectory=/opt/tiemeasureflow/src/frontend/flask_app
Environment="PATH=/opt/tiemeasureflow/venv/bin" EnvironmentFile=/opt/tiemeasureflow/.env
Environment="FLASK_ENV=production" ExecStart=/opt/tiemeasureflow/.venv/bin/uv run --project /opt/tiemeasureflow gunicorn \
ExecStart=/opt/tiemeasureflow/venv/bin/gunicorn \ --workers 5 --threads 4 --worker-class gthread --timeout 60 \
--workers 4 \ --bind 0.0.0.0:5000 --forwarded-allow-ips='*' \
--bind 0.0.0.0:5000 \ "app:create_app()"
app:app
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10
@@ -373,8 +335,9 @@ WantedBy=multi-user.target
### Security Checklist ### Security Checklist
- [ ] Change all default passwords in `.env` - [ ] Change all default passwords in `.env` — con `docker-compose.yml`, `DB_ROOT_PASSWORD`, `DB_PASSWORD`, `SERVER_SECRET_KEY`, `CLIENT_SECRET_KEY` non hanno più un default: `docker compose config` si rifiuta di partire se mancano (introdotto dalla revisione del 21/09/2026)
- [ ] Generate random `SERVER_SECRET_KEY` (32+ characters) - [ ] Generate random `SERVER_SECRET_KEY` (32+ characters)
- [ ] Set `APP_DOMAIN` to the real public domain (used by the Traefik router rules)
- [ ] Set CORS origins to actual client domains - [ ] Set CORS origins to actual client domains
- [ ] Enable SSL/HTTPS (see SSL/TLS section) - [ ] Enable SSL/HTTPS (see SSL/TLS section)
- [ ] Configure firewall rules - [ ] Configure firewall rules
@@ -386,200 +349,116 @@ WantedBy=multi-user.target
### Recommended Architecture ### Recommended Architecture
Il progetto usa due reverse proxy diversi a seconda dell'installazione, non uno
generico da configurare a mano:
- **On-premise, rete LAN, HTTP semplice** (`docker-compose.dev.yml`): Nginx,
configurazione già pronta in [`nginx/nginx.conf`](../nginx/nginx.conf). È il
caso di una stazione di misura sul reparto, senza certificato TLS.
- **Produzione, dominio pubblico** (`docker-compose.yml`): Traefik, con
certificati Let's Encrypt automatici. La configurazione sta nelle label
Docker del servizio, non in un file a parte — vedi la sezione
[Docker Deployment](#docker-deployment) sotto.
``` ```
┌─────────────────┐ ┌──────────────────────────┐
│ Nginx Proxy │ (Port 80/443) On-premise (LAN) │ Nginx (:80) │
└────────┬────────┘ └─────────────┬────────────┘
│ │
┌────┴────┐ Produzione (dominio pubblico) → Traefik (:80/:443, TLS Let's Encrypt)
│ │ │
┌───▼──┐ ┌───▼──┐ ┌───────────┴───────────┐
│API │ │Web │ │ │
│:8000 │ │:5000 │ ┌─────▼─────┐ ┌──────▼─────┐
└──────┘ └──────┘ │ server │ │ client │
│ │ │ :8000 │ │ :5000 │
└────┬─────┘ └─────┬──────┘ └────────────┘
│ │
┌────▼────┐ ┌─────▼──────┐ ┌─────────────┐
│ MySQL │ │ vision │ │ MySQL 8.0 │
│ :3306 │ │ :8100 │ │ :3306 │
└─────────┘ │ (interno) │ └─────────────┘
└─────────────┘
``` ```
### Nginx Configuration Il worker di visione (`vision`) non pubblica porte verso l'esterno: lo
raggiunge solo il `server`, sulla rete Docker interna `tmflow-net`.
Create `/etc/nginx/sites-available/tiemeasureflow`:
```nginx
upstream tiemeasureflow_api {
server 127.0.0.1:8000;
}
upstream tiemeasureflow_web {
server 127.0.0.1:5000;
}
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/ssl/certs/yourdomain.com.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 50M;
# API
location /api/ {
proxy_pass http://tiemeasureflow_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Web Client
location / {
proxy_pass http://tiemeasureflow_web;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Static files (optional caching)
location /static/ {
proxy_pass http://tiemeasureflow_web;
expires 1d;
add_header Cache-Control "public, immutable";
}
}
```
Enable and test:
```bash
sudo ln -s /etc/nginx/sites-available/tiemeasureflow /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```
--- ---
## Docker Deployment ## Docker Deployment
### Docker Compose (Complete Stack) Docker Compose è il metodo raccomandato ed è quello usato in produzione. Il
progetto ha **tre Dockerfile alla radice** del repository — non uno a
sottocartella:
The project includes a `docker-compose.yml` for easy deployment: | File | Servizio | Base |
|---|---|---|
| `Dockerfile` | `server` (FastAPI) | `python:3.11.16-slim` |
| `Dockerfile.frontend` | `client` (Flask) | `python:3.11.16-slim`, TailwindCSS compilato in uno stage `node:20.20.2-bookworm-slim` |
| `Dockerfile.vision` | `vision` (worker di visione) | `python:3.13.15-slim` |
E due file compose:
- **`docker-compose.dev.yml`** — sviluppo/on-premise, con Nginx (vedi
[Recommended Architecture](#recommended-architecture) sopra). 4 servizi:
`mysql`, `server`, `vision`, `client`, `nginx`.
- **`docker-compose.yml`** — produzione, con Traefik e TLS automatico. 4
servizi: `mysql`, `server`, `vision`, `client`. Richiede la rete Docker
esterna `traefik` (vedi `networks.traefik-net` nel file) e le variabili
`APP_DOMAIN`, `DB_ROOT_PASSWORD`, `DB_PASSWORD`, `SERVER_SECRET_KEY`,
`CLIENT_SECRET_KEY` valorizzate — senza default, `docker compose config` si
rifiuta di procedere se mancano.
Il worker di visione dipende dal submodule `vendor/visionsuite`
(`git clone --recurse-submodules`, vedi il [Quick Start del README](../README.md#quick-start-con-docker))
e richiede `VISION_ENGINE_VERSION` valorizzata **prima** della build, altrimenti
`Dockerfile.vision` fallisce di proposito:
```bash ```bash
# Build images # Sviluppo / on-premise
docker-compose build VISION_ENGINE_VERSION=$(git -C vendor/visionsuite rev-parse HEAD) \
docker compose -f docker-compose.dev.yml up -d --build
# Start services # Produzione
docker-compose up -d VISION_ENGINE_VERSION=$(git -C vendor/visionsuite rev-parse HEAD) \
docker compose -f docker-compose.yml up -d --build
# View logs
docker-compose logs -f
# Stop services
docker-compose down
``` ```
### Manual Docker Build Comandi comuni (sintassi `docker compose` v2, senza trattino — il plugin, non
il vecchio `docker-compose` standalone):
#### API Server
```dockerfile
FROM python:3.11-slim
WORKDIR /app/server
COPY server/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ .
COPY .env ..
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
Build and run:
```bash ```bash
docker build -f server/Dockerfile -t tiemeasureflow-api . docker compose -f docker-compose.yml ps
docker run -p 8000:8000 --env-file .env tiemeasureflow-api docker compose -f docker-compose.yml logs -f server
``` docker compose -f docker-compose.yml down
#### Web Client
```dockerfile
FROM python:3.11-slim
WORKDIR /app/client
COPY client/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY client/ .
COPY .env ..
EXPOSE 5000
CMD ["gunicorn", "--workers", "4", "--bind", "0.0.0.0:5000", "app:app"]
```
Build and run:
```bash
docker build -f client/Dockerfile -t tiemeasureflow-web .
docker run -p 5000:5000 --env-file .env tiemeasureflow-web
``` ```
--- ---
## SSL/TLS ## SSL/TLS
### Self-Signed Certificate (Development) Le due installazioni gestiscono il TLS in due modi diversi, e nessuno dei due
richiede un certificato generato a mano:
```bash - **Produzione** (`docker-compose.yml`): Traefik ottiene e rinnova da solo un
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes certificato Let's Encrypt per `APP_DOMAIN`, tramite il `certresolver`
``` configurato nelle label del servizio (`tls.certresolver=mytlschallenge`).
Serve solo che `APP_DOMAIN` punti davvero al server (DNS) e che le porte
80/443 siano raggiungibili da internet per la sfida ACME.
- **On-premise, rete LAN** (`docker-compose.dev.yml`): Nginx serve in HTTP
semplice, senza TLS — è il caso descritto in
[Recommended Architecture](#recommended-architecture). Chi ha comunque
bisogno di un certificato (es. una CA interna) lo aggiunge scommentando la
sezione SSL già presente, ma commentata, in
[`nginx/nginx.conf`](../nginx/nginx.conf).
### Let's Encrypt (Production) Un'installazione manuale (senza Docker, sezioni [Server](#server-deployment) e
[Client Deployment](#client-deployment) sopra) che debba terminare il TLS da
```bash sola, dietro il proprio reverse proxy, può ancora valorizzare
sudo apt install certbot python3-certbot-nginx `SSL_CERTFILE`/`SSL_KEYFILE` in `.env` — variabili lasciate per questo caso,
sudo certbot certonly --nginx -d yourdomain.com non lette da uvicorn/gunicorn nei comandi mostrati sopra: vanno passate al
``` proprio reverse proxy o gestite fuori da questo progetto.
### Configure in .env
```env
SSL_CERTFILE=/etc/ssl/certs/yourdomain.com.crt
SSL_KEYFILE=/etc/ssl/private/yourdomain.com.key
```
### Start Server with SSL
```bash
cd server
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8443 \
--certfile /etc/ssl/certs/yourdomain.com.crt \
--keyfile /etc/ssl/private/yourdomain.com.key
```
--- ---
@@ -644,14 +523,11 @@ sqlalchemy.exc.OperationalError: (asyncmy.errors.ProgrammingError) (2003, "Can't
**Check:** **Check:**
```bash ```bash
# Verify MySQL is running # Verify MySQL is running (con Docker: docker compose ps mysql, ha già un healthcheck)
mysql -u tmflow -p -e "SELECT 1" mysql -u tmflow -p -e "SELECT 1"
# Check .env variables # Check .env variables — non stampare mai la password: si controlla il valore, non la connection string
grep DB_ .env grep DB_ .env
# Test connection string
cd server && python -c "from config import settings; print(settings.database_url)"
``` ```
### Port Already in Use ### Port Already in Use
@@ -667,7 +543,7 @@ lsof -i :8000
kill -9 <PID> kill -9 <PID>
# Or use different port # Or use different port
uvicorn main:app --port 8001 uv run uvicorn src.backend.main:app --port 8001
``` ```
### Import Errors ### Import Errors
@@ -678,21 +554,20 @@ ModuleNotFoundError: No module named 'fastapi'
**Solution:** **Solution:**
```bash ```bash
cd server uv sync --extra server --extra client --extra dev
pip install -r requirements.txt
``` ```
### File Upload Not Working ### File Upload Not Working
``` ```
/app/server/uploads: Permission denied uploads: Permission denied
``` ```
**Solution:** **Solution:**
```bash ```bash
# Create uploads directory with correct permissions # Create uploads directory with correct permissions (radice del repo, non server/uploads)
mkdir -p server/uploads mkdir -p uploads/images uploads/pdfs uploads/logos uploads/reports
chmod 755 server/uploads chmod 755 uploads
``` ```
### Migrations Failed ### Migrations Failed
@@ -703,13 +578,12 @@ alembic.util.exc.CommandError: Can't locate revision identified by...
**Solution:** **Solution:**
```bash ```bash
cd server
# Check migration status # Check migration status
alembic current uv run alembic -c src/backend/migrations/alembic.ini current
# Reset migrations (CAUTION - deletes data) # Reset migrations (CAUTION - deletes data)
alembic downgrade base uv run alembic -c src/backend/migrations/alembic.ini downgrade base
alembic upgrade head uv run alembic -c src/backend/migrations/alembic.ini upgrade head
``` ```
### API Key Not Working ### API Key Not Working