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:
+157
-283
@@ -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 │
|
│ server │ │ client │
|
||||||
│ :8000 │ │ :5000 │
|
│ :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
|
||||||
|
|||||||
Reference in New Issue
Block a user