From ac11cec27692e4999ac94b745e64bab05f2d7225 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 21 Sep 2026 22:45:55 +0200 Subject: [PATCH] docs(deployment): riscrive le sezioni con percorsi morti sul layout reale (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/DEPLOYMENT.md | 448 ++++++++++++++++----------------------------- 1 file changed, 161 insertions(+), 287 deletions(-) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index dcc3884..8d0d302 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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) -└────────┬────────┘ - │ - ┌────┴────┐ - │ │ -┌───▼──┐ ┌───▼──┐ -│API │ │Web │ -│:8000 │ │:5000 │ -└──────┘ └──────┘ - │ │ - └────┬─────┘ - │ - ┌────▼────┐ - │ MySQL │ - │ :3306 │ - └─────────┘ + ┌──────────────────────────┐ +On-premise (LAN) │ Nginx (:80) │ + └─────────────┬────────────┘ + │ +Produzione (dominio pubblico) → Traefik (:80/:443, TLS Let's Encrypt) + │ + ┌───────────┴───────────┐ + │ │ + ┌─────▼─────┐ ┌──────▼─────┐ + │ 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 # 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