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:
+159
-285
@@ -142,6 +142,18 @@ SSL_KEYFILE=
|
||||
| `SSL_CERTFILE` | string | (empty) | Path to SSL certificate (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
|
||||
@@ -169,63 +181,33 @@ EXIT;
|
||||
|
||||
### 2. Run Database Migrations
|
||||
|
||||
```bash
|
||||
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:
|
||||
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/`.
|
||||
|
||||
```bash
|
||||
cd server
|
||||
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())
|
||||
"
|
||||
uv run alembic -c src/backend/migrations/alembic.ini upgrade head
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
cd server
|
||||
pip install -r requirements.txt
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
uv sync --extra server --extra dev
|
||||
uv run uvicorn src.backend.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
Runs at `http://0.0.0.0:8000`
|
||||
@@ -234,40 +216,15 @@ API Documentation:
|
||||
- Swagger UI: `http://localhost:8000/docs`
|
||||
- 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
|
||||
pip install gunicorn
|
||||
```
|
||||
|
||||
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
|
||||
uv run uvicorn src.backend.main:app \
|
||||
--host 0.0.0.0 --port 8000 --workers ${WEB_CONCURRENCY:-4} \
|
||||
--proxy-headers --forwarded-allow-ips='*'
|
||||
```
|
||||
|
||||
### systemd Service (Linux)
|
||||
@@ -280,14 +237,14 @@ Description=TieMeasureFlow API Server
|
||||
After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
Type=simple
|
||||
User=tiemeasureflow
|
||||
WorkingDirectory=/opt/tiemeasureflow/server
|
||||
Environment="PATH=/opt/tiemeasureflow/venv/bin"
|
||||
ExecStart=/opt/tiemeasureflow/venv/bin/gunicorn main:app \
|
||||
--workers 4 \
|
||||
--worker-class uvicorn.workers.UvicornWorker \
|
||||
--bind 0.0.0.0:8000
|
||||
WorkingDirectory=/opt/tiemeasureflow
|
||||
EnvironmentFile=/opt/tiemeasureflow/.env
|
||||
Environment="WEB_CONCURRENCY=4"
|
||||
ExecStart=/opt/tiemeasureflow/.venv/bin/uv run uvicorn src.backend.main:app \
|
||||
--host 0.0.0.0 --port 8000 --workers ${WEB_CONCURRENCY} \
|
||||
--proxy-headers --forwarded-allow-ips='*'
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
@@ -315,12 +272,14 @@ sudo journalctl -u tiemeasureflow-api -f
|
||||
|
||||
## Client Deployment
|
||||
|
||||
Anche qui, Docker Compose è il metodo raccomandato; questa sezione è per lo
|
||||
sviluppo o un'installazione manuale senza container.
|
||||
|
||||
### Development Server
|
||||
|
||||
```bash
|
||||
cd client
|
||||
pip install -r requirements.txt
|
||||
flask run --host 0.0.0.0 --port 5000
|
||||
uv sync --extra client --extra dev
|
||||
cd src/frontend/flask_app && uv run --project ../../.. flask run --host 0.0.0.0 --port 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)
|
||||
|
||||
```bash
|
||||
cd client
|
||||
cd src/frontend/flask_app
|
||||
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
|
||||
pip install gunicorn
|
||||
cd client
|
||||
gunicorn --workers 4 --bind 0.0.0.0:5000 app:app
|
||||
cd src/frontend/flask_app && uv run --project ../../.. gunicorn \
|
||||
--workers 5 --threads 4 --worker-class gthread --timeout 60 \
|
||||
--bind 0.0.0.0:5000 --forwarded-allow-ips='*' \
|
||||
"app:create_app()"
|
||||
```
|
||||
|
||||
### systemd Service (Linux)
|
||||
@@ -350,15 +313,14 @@ Description=TieMeasureFlow Web Client
|
||||
After=network.target tiemeasureflow-api.service
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
Type=simple
|
||||
User=tiemeasureflow
|
||||
WorkingDirectory=/opt/tiemeasureflow/client
|
||||
Environment="PATH=/opt/tiemeasureflow/venv/bin"
|
||||
Environment="FLASK_ENV=production"
|
||||
ExecStart=/opt/tiemeasureflow/venv/bin/gunicorn \
|
||||
--workers 4 \
|
||||
--bind 0.0.0.0:5000 \
|
||||
app:app
|
||||
WorkingDirectory=/opt/tiemeasureflow/src/frontend/flask_app
|
||||
EnvironmentFile=/opt/tiemeasureflow/.env
|
||||
ExecStart=/opt/tiemeasureflow/.venv/bin/uv run --project /opt/tiemeasureflow gunicorn \
|
||||
--workers 5 --threads 4 --worker-class gthread --timeout 60 \
|
||||
--bind 0.0.0.0:5000 --forwarded-allow-ips='*' \
|
||||
"app:create_app()"
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
@@ -373,8 +335,9 @@ WantedBy=multi-user.target
|
||||
|
||||
### 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)
|
||||
- [ ] Set `APP_DOMAIN` to the real public domain (used by the Traefik router rules)
|
||||
- [ ] Set CORS origins to actual client domains
|
||||
- [ ] Enable SSL/HTTPS (see SSL/TLS section)
|
||||
- [ ] Configure firewall rules
|
||||
@@ -386,200 +349,116 @@ WantedBy=multi-user.target
|
||||
|
||||
### 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) │
|
||||
└─────────────┬────────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ │
|
||||
┌───▼──┐ ┌───▼──┐
|
||||
│API │ │Web │
|
||||
│:8000 │ │:5000 │
|
||||
└──────┘ └──────┘
|
||||
│ │
|
||||
└────┬─────┘
|
||||
Produzione (dominio pubblico) → Traefik (:80/:443, TLS Let's Encrypt)
|
||||
│
|
||||
┌────▼────┐
|
||||
│ MySQL │
|
||||
│ :3306 │
|
||||
└─────────┘
|
||||
┌───────────┴───────────┐
|
||||
│ │
|
||||
┌─────▼─────┐ ┌──────▼─────┐
|
||||
│ server │ │ client │
|
||||
│ :8000 │ │ :5000 │
|
||||
└─────┬──────┘ └────────────┘
|
||||
│
|
||||
┌─────▼──────┐ ┌─────────────┐
|
||||
│ vision │ │ MySQL 8.0 │
|
||||
│ :8100 │ │ :3306 │
|
||||
│ (interno) │ └─────────────┘
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
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
|
||||
```
|
||||
Il worker di visione (`vision`) non pubblica porte verso l'esterno: lo
|
||||
raggiunge solo il `server`, sulla rete Docker interna `tmflow-net`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
# Build images
|
||||
docker-compose build
|
||||
# Sviluppo / on-premise
|
||||
VISION_ENGINE_VERSION=$(git -C vendor/visionsuite rev-parse HEAD) \
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
# Produzione
|
||||
VISION_ENGINE_VERSION=$(git -C vendor/visionsuite rev-parse HEAD) \
|
||||
docker compose -f docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
### Manual Docker Build
|
||||
|
||||
#### 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:
|
||||
Comandi comuni (sintassi `docker compose` v2, senza trattino — il plugin, non
|
||||
il vecchio `docker-compose` standalone):
|
||||
|
||||
```bash
|
||||
docker build -f server/Dockerfile -t tiemeasureflow-api .
|
||||
docker run -p 8000:8000 --env-file .env tiemeasureflow-api
|
||||
```
|
||||
|
||||
#### 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
|
||||
docker compose -f docker-compose.yml ps
|
||||
docker compose -f docker-compose.yml logs -f server
|
||||
docker compose -f docker-compose.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes
|
||||
```
|
||||
- **Produzione** (`docker-compose.yml`): Traefik ottiene e rinnova da solo un
|
||||
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)
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot certonly --nginx -d yourdomain.com
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
Un'installazione manuale (senza Docker, sezioni [Server](#server-deployment) e
|
||||
[Client Deployment](#client-deployment) sopra) che debba terminare il TLS da
|
||||
sola, dietro il proprio reverse proxy, può ancora valorizzare
|
||||
`SSL_CERTFILE`/`SSL_KEYFILE` in `.env` — variabili lasciate per questo caso,
|
||||
non lette da uvicorn/gunicorn nei comandi mostrati sopra: vanno passate al
|
||||
proprio reverse proxy o gestite fuori da questo progetto.
|
||||
|
||||
---
|
||||
|
||||
@@ -644,14 +523,11 @@ sqlalchemy.exc.OperationalError: (asyncmy.errors.ProgrammingError) (2003, "Can't
|
||||
|
||||
**Check:**
|
||||
```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"
|
||||
|
||||
# Check .env variables
|
||||
# Check .env variables — non stampare mai la password: si controlla il valore, non la connection string
|
||||
grep DB_ .env
|
||||
|
||||
# Test connection string
|
||||
cd server && python -c "from config import settings; print(settings.database_url)"
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
@@ -667,7 +543,7 @@ lsof -i :8000
|
||||
kill -9 <PID>
|
||||
|
||||
# Or use different port
|
||||
uvicorn main:app --port 8001
|
||||
uv run uvicorn src.backend.main:app --port 8001
|
||||
```
|
||||
|
||||
### Import Errors
|
||||
@@ -678,21 +554,20 @@ ModuleNotFoundError: No module named 'fastapi'
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
cd server
|
||||
pip install -r requirements.txt
|
||||
uv sync --extra server --extra client --extra dev
|
||||
```
|
||||
|
||||
### File Upload Not Working
|
||||
|
||||
```
|
||||
/app/server/uploads: Permission denied
|
||||
uploads: Permission denied
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Create uploads directory with correct permissions
|
||||
mkdir -p server/uploads
|
||||
chmod 755 server/uploads
|
||||
# Create uploads directory with correct permissions (radice del repo, non server/uploads)
|
||||
mkdir -p uploads/images uploads/pdfs uploads/logos uploads/reports
|
||||
chmod 755 uploads
|
||||
```
|
||||
|
||||
### Migrations Failed
|
||||
@@ -703,13 +578,12 @@ alembic.util.exc.CommandError: Can't locate revision identified by...
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
cd server
|
||||
# Check migration status
|
||||
alembic current
|
||||
uv run alembic -c src/backend/migrations/alembic.ini current
|
||||
|
||||
# Reset migrations (CAUTION - deletes data)
|
||||
alembic downgrade base
|
||||
alembic upgrade head
|
||||
uv run alembic -c src/backend/migrations/alembic.ini downgrade base
|
||||
uv run alembic -c src/backend/migrations/alembic.ini upgrade head
|
||||
```
|
||||
|
||||
### API Key Not Working
|
||||
|
||||
Reference in New Issue
Block a user