# TieMeasureFlow Deployment Guide This guide covers deploying TieMeasureFlow in development, staging, and production environments. ## Table of Contents 1. [Prerequisites](#prerequisites) 2. [Environment Setup](#environment-setup) 3. [Database Setup](#database-setup) 4. [Server Deployment](#server-deployment) 5. [Client Deployment](#client-deployment) 6. [Production Deployment](#production-deployment) 7. [Docker Deployment](#docker-deployment) 8. [SSL/HTTPS](#ssltls) 9. [Backup Strategy](#backup-strategy) 10. [Troubleshooting](#troubleshooting) ## Prerequisites ### System Requirements - **OS**: Linux, macOS, or Windows - **Python**: 3.11 or higher - **MySQL**: 8.0 or higher - **Node.js**: 16+ (optional, for Tailwind CSS compilation) - **Disk Space**: 500 MB minimum - **RAM**: 2 GB minimum ### Software Installation #### Linux/macOS ```bash # Python 3.11+ python3 --version # MySQL 8.0 mysql --version # Node.js (optional) node --version npm --version ``` #### Windows Download and install: - [Python 3.11+](https://www.python.org/downloads/) - [MySQL Community Server](https://dev.mysql.com/downloads/mysql/) - [Node.js](https://nodejs.org/) (optional) --- ## Environment Setup ### 1. Clone Repository ```bash git clone cd TieMeasureFlow ``` ### 2. Create .env File Copy the example and customize for your environment: ```bash cp .env.example .env ``` ### 3. Configure .env Edit `.env` with your settings: ```env # ===================================================================== # DATABASE # ===================================================================== DB_HOST=localhost DB_PORT=3306 DB_NAME=tiemeasureflow DB_USER=tmflow DB_PASSWORD=change_me_in_production # ===================================================================== # SERVER (FastAPI) # ===================================================================== SERVER_HOST=0.0.0.0 SERVER_PORT=8000 SERVER_SECRET_KEY=change-this-to-a-random-secret-key-with-32-chars # ===================================================================== # CLIENT (Flask) # ===================================================================== CLIENT_HOST=0.0.0.0 CLIENT_PORT=5000 # ===================================================================== # CORS # ===================================================================== SERVER_CORS_ORIGINS=http://localhost:5000,http://127.0.0.1:5000 # ===================================================================== # FILE UPLOAD # ===================================================================== UPLOAD_DIR=uploads MAX_UPLOAD_SIZE_MB=50 # ===================================================================== # RATE LIMITING # ===================================================================== RATE_LIMIT_LOGIN=5 RATE_LIMIT_GENERAL=100 # ===================================================================== # SSL/HTTPS (Production only) # ===================================================================== SSL_CERTFILE= SSL_KEYFILE= ``` ### Environment Variables Reference | Variable | Type | Default | Description | |----------|------|---------|-------------| | `DB_HOST` | string | localhost | MySQL server hostname | | `DB_PORT` | int | 3306 | MySQL server port | | `DB_NAME` | string | tiemeasureflow | Database name | | `DB_USER` | string | tmflow | Database user | | `DB_PASSWORD` | string | change_me_in_production | Database password **[CHANGE IN PROD]** | | `SERVER_HOST` | string | 0.0.0.0 | API server bind address | | `SERVER_PORT` | int | 8000 | API server port | | `SERVER_SECRET_KEY` | string | change-this-to... | Secret key for sessions **[CHANGE IN PROD]** | | `CLIENT_HOST` | string | 0.0.0.0 | Flask client bind address | | `CLIENT_PORT` | int | 5000 | Flask client port | | `SERVER_CORS_ORIGINS` | string | http://localhost:5000 | Comma-separated CORS origins | | `UPLOAD_DIR` | string | uploads | Directory for file uploads (resolved against the project root) | | `MAX_UPLOAD_SIZE_MB` | int | 50 | Maximum upload file size in MB | | `RATE_LIMIT_LOGIN` | int | 5 | Login requests per minute, per real client IP | | `RATE_LIMIT_GENERAL` | int | 300 | General requests per minute, per real client IP (post-V2.0.0; was 100 in V1.0.x) | | `STATION_CODE` | string | (empty) | **Per-tablet** code identifying the station this Flask client serves. Must match a station created in the admin UI. Empty = the client refuses `/measure/select` with HTTP 503 "Stazione non configurata". | | `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_ENV` | string | dev | Ambiente di esecuzione. `dev`/`test` saltano il controllo sui segreti di default; qualsiasi altro valore lo attiva. `docker-compose.yml` lo imposta a `production`, `docker-compose.dev.yml` a `dev` | | `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 ### 1. Create MySQL Database and User ```bash mysql -u root -p ``` ```sql -- Create database CREATE DATABASE tiemeasureflow CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Create user CREATE USER 'tmflow'@'localhost' IDENTIFIED BY 'secure_password_here'; -- Grant permissions GRANT ALL PRIVILEGES ON tiemeasureflow.* TO 'tmflow'@'localhost'; FLUSH PRIVILEGES; -- Exit EXIT; ``` ### 2. Run Database Migrations 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 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 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` API Documentation: - Swagger UI: `http://localhost:8000/docs` - ReDoc: `http://localhost:8000/redoc` ### Production, senza Docker Lo stesso comando che gira dentro il container (`Dockerfile`), lanciato dalla radice del repository con l'interprete e il venv pinnati da `uv`: ```bash 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) Create `/etc/systemd/system/tiemeasureflow-api.service`: ```ini [Unit] Description=TieMeasureFlow API Server After=network.target mysql.service [Service] Type=simple User=tiemeasureflow 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 [Install] WantedBy=multi-user.target ``` Enable and start: ```bash sudo systemctl daemon-reload sudo systemctl enable tiemeasureflow-api.service sudo systemctl start tiemeasureflow-api.service sudo systemctl status tiemeasureflow-api.service ``` View logs: ```bash 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 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` ### Compile Tailwind CSS (Optional) ```bash cd src/frontend/flask_app npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch ``` ### Production, senza Docker Lo stesso comando del `Dockerfile.frontend` (gunicorn richiede la cwd dentro `flask_app` per risolvere `app:create_app()`): ```bash 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) Create `/etc/systemd/system/tiemeasureflow-web.service`: ```ini [Unit] Description=TieMeasureFlow Web Client After=network.target tiemeasureflow-api.service [Service] Type=simple User=tiemeasureflow 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 [Install] WantedBy=multi-user.target ``` --- ## Production Deployment ### Security Checklist - [ ] 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 - [ ] Set up regular database backups - [ ] Configure log rotation - [ ] Use strong database credentials - [ ] Restrict file upload sizes - [ ] Keep dependencies updated ### 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. ``` ┌──────────────────────────┐ 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) │ └─────────────┘ └─────────────┘ ``` 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 è il metodo raccomandato ed è quello usato in produzione. Il progetto ha **tre Dockerfile alla radice** del repository — non uno a sottocartella: | 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 # Sviluppo / on-premise VISION_ENGINE_VERSION=$(git -C vendor/visionsuite rev-parse HEAD) \ docker compose -f docker-compose.dev.yml up -d --build # Produzione VISION_ENGINE_VERSION=$(git -C vendor/visionsuite rev-parse HEAD) \ docker compose -f docker-compose.yml up -d --build ``` Comandi comuni (sintassi `docker compose` v2, senza trattino — il plugin, non il vecchio `docker-compose` standalone): ```bash 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 Le due installazioni gestiscono il TLS in due modi diversi, e nessuno dei due richiede un certificato generato a mano: - **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). 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. --- ## Backup Strategy ### Daily Database Backups Create backup script `/opt/tiemeasureflow/backup.sh`: ```bash #!/bin/bash BACKUP_DIR="/opt/tiemeasureflow/backups" TIMESTAMP=$(date +%Y%m%d_%H%M%S) DB_NAME="tiemeasureflow" DB_USER="tmflow" mkdir -p $BACKUP_DIR # MySQL dump mysqldump -u $DB_USER -p$MYSQL_PASSWORD $DB_NAME \ | gzip > $BACKUP_DIR/db_$TIMESTAMP.sql.gz # Keep only last 30 days find $BACKUP_DIR -name "db_*.sql.gz" -mtime +30 -delete # Upload to S3 (optional) # aws s3 cp $BACKUP_DIR/db_$TIMESTAMP.sql.gz s3://your-bucket/backups/ echo "Backup completed: $BACKUP_DIR/db_$TIMESTAMP.sql.gz" ``` Schedule with crontab: ```bash crontab -e ``` Add line: ```cron 0 2 * * * /opt/tiemeasureflow/backup.sh >> /var/log/tiemeasureflow/backup.log 2>&1 ``` ### Restore from Backup ```bash # Decompress gunzip < backups/db_20250207_020000.sql.gz | \ mysql -u tmflow -p tiemeasureflow ``` --- ## Troubleshooting ### Database Connection Failed ``` sqlalchemy.exc.OperationalError: (asyncmy.errors.ProgrammingError) (2003, "Can't connect to MySQL server...") ``` **Check:** ```bash # Verify MySQL is running (con Docker: docker compose ps mysql, ha già un healthcheck) mysql -u tmflow -p -e "SELECT 1" # Check .env variables — non stampare mai la password: si controlla il valore, non la connection string grep DB_ .env ``` ### Port Already in Use ``` OSError: [Errno 48] Address already in use ``` **Solution:** ```bash # Find process on port 8000 lsof -i :8000 kill -9 # Or use different port uv run uvicorn src.backend.main:app --port 8001 ``` ### Import Errors ``` ModuleNotFoundError: No module named 'fastapi' ``` **Solution:** ```bash uv sync --extra server --extra client --extra dev ``` ### File Upload Not Working ``` uploads: Permission denied ``` **Solution:** ```bash # 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 ``` alembic.util.exc.CommandError: Can't locate revision identified by... ``` **Solution:** ```bash # Check migration status uv run alembic -c src/backend/migrations/alembic.ini current # Reset migrations (CAUTION - deletes data) 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 ``` detail: "Invalid API key" ``` **Solution:** ```bash # Regenerate API key for user (admin endpoint) curl -X POST http://localhost:8000/api/users/1/regenerate-key \ -H "X-API-Key: admin_token" ``` --- ## Performance Tuning ### MySQL Edit `/etc/mysql/mysql.conf.d/mysqld.cnf`: ```ini [mysqld] # Connection pool max_connections = 100 # Buffer sizes innodb_buffer_pool_size = 256M innodb_log_file_size = 100M # Query optimization query_cache_size = 0 query_cache_type = 0 # Logging slow_query_log = 1 long_query_time = 2 ``` Restart: ```bash sudo systemctl restart mysql ``` ### Application Workers Increase Gunicorn workers (rule: 2 * CPU_cores + 1): ```bash gunicorn --workers 9 ... # For 4-core system ``` ### Nginx Caching Add to nginx config: ```nginx proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m; location /api/statistics/ { proxy_cache api_cache; proxy_cache_valid 200 10m; add_header X-Cache-Status $upstream_cache_status; } ``` --- ## Monitoring & Logging ### Application Logs ```bash # API server tail -f /var/log/tiemeasureflow/api_error.log # Web client tail -f /var/log/tiemeasureflow/web_error.log # System journalctl -u tiemeasureflow-api -f ``` ### Health Check Endpoint ```bash curl http://localhost:8000/api/health ``` Response: ```json { "status": "ok", "service": "TieMeasureFlow API", "version": "0.1.0" } ``` ### Database Connection Pooling Monitor with: ```bash # MySQL connections mysql -e "SHOW PROCESSLIST;" | grep tiemeasureflow ```