Files
TieMeasureFlow/docs/DEPLOYMENT.md
T
Adriano ac11cec276 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>
2026-09-21 22:45:55 +02:00

20 KiB

TieMeasureFlow Deployment Guide

This guide covers deploying TieMeasureFlow in development, staging, and production environments.

Table of Contents

  1. Prerequisites
  2. Environment Setup
  3. Database Setup
  4. Server Deployment
  5. Client Deployment
  6. Production Deployment
  7. Docker Deployment
  8. SSL/HTTPS
  9. Backup Strategy
  10. 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

# Python 3.11+
python3 --version

# MySQL 8.0
mysql --version

# Node.js (optional)
node --version
npm --version

Windows

Download and install:


Environment Setup

1. Clone Repository

git clone <repository-url>
cd TieMeasureFlow

2. Create .env File

Copy the example and customize for your environment:

cp .env.example .env

3. Configure .env

Edit .env with your settings:

# =====================================================================
# 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_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

Database Setup

1. Create MySQL Database and User

mysql -u root -p
-- 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/.

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.

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

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:

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:

[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:

sudo systemctl daemon-reload
sudo systemctl enable tiemeasureflow-api.service
sudo systemctl start tiemeasureflow-api.service
sudo systemctl status tiemeasureflow-api.service

View logs:

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

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)

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()):

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:

[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

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. È 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 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 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) e richiede VISION_ENGINE_VERSION valorizzata prima della build, altrimenti Dockerfile.vision fallisce di proposito:

# 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):

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. 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.

Un'installazione manuale (senza Docker, sezioni Server e 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:

#!/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:

crontab -e

Add line:

0 2 * * * /opt/tiemeasureflow/backup.sh >> /var/log/tiemeasureflow/backup.log 2>&1

Restore from Backup

# 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:

# 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:

# Find process on port 8000
lsof -i :8000
kill -9 <PID>

# Or use different port
uv run uvicorn src.backend.main:app --port 8001

Import Errors

ModuleNotFoundError: No module named 'fastapi'

Solution:

uv sync --extra server --extra client --extra dev

File Upload Not Working

uploads: Permission denied

Solution:

# 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:

# 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:

# 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:

[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:

sudo systemctl restart mysql

Application Workers

Increase Gunicorn workers (rule: 2 * CPU_cores + 1):

gunicorn --workers 9 ...  # For 4-core system

Nginx Caching

Add to nginx config:

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

# 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

curl http://localhost:8000/api/health

Response:

{
  "status": "ok",
  "service": "TieMeasureFlow API",
  "version": "0.1.0"
}

Database Connection Pooling

Monitor with:

# MySQL connections
mysql -e "SHOW PROCESSLIST;" | grep tiemeasureflow