useradd senza -m lascia l'utente app senza $HOME: uv run nel CMD non trova dove scrivere la sua cache. Aggiunto -m a useradd in Dockerfile, Dockerfile.frontend e Dockerfile.vision, e UV_CACHE_DIR=/tmp/uv-cache esplicito (indipendente da $HOME). docs/DEPLOYMENT.md: nuova sezione sull'aggiornamento di un'installazione esistente, con il chown del volume upload_data ereditato da versioni precedenti. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
22 KiB
TieMeasureFlow Deployment Guide
This guide covers deploying TieMeasureFlow in development, staging, and production environments.
Table of Contents
- Prerequisites
- Environment Setup
- Database Setup
- Server Deployment
- Client Deployment
- Production Deployment
- Docker Deployment
- SSL/HTTPS
- Backup Strategy
- 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:
- Python 3.11+
- MySQL Community Server
- Node.js (optional)
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_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 |
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— condocker-compose.yml,DB_ROOT_PASSWORD,DB_PASSWORD,SERVER_SECRET_KEY,CLIENT_SECRET_KEYnon hanno più un default:docker compose configsi rifiuta di partire se mancano (introdotto dalla revisione del 21/09/2026) - Generate random
SERVER_SECRET_KEY(32+ characters) - Set
APP_DOMAINto 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 innginx/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 esternatraefik(vedinetworks.traefik-netnel file) e le variabiliAPP_DOMAIN,DB_ROOT_PASSWORD,DB_PASSWORD,SERVER_SECRET_KEY,CLIENT_SECRET_KEYvalorizzate — senza default,docker compose configsi 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
Aggiornare un'installazione esistente
Le immagini precedenti creavano l'utente non privilegiato senza una home
directory (useradd -u 10001 app), e il volume upload_data che ne è
risultato appartiene a root. Le immagini attuali creano l'utente con
-m e impostano UV_CACHE_DIR=/tmp/uv-cache, ma questo non cambia da solo
la proprietà di un volume già esistente: se si riparte con le nuove immagini
su un volume creato da una versione precedente, il container server non
riesce a scrivere sotto /app/uploads.
Prima del primo avvio con le nuove immagini, va quindi corretta la proprietà del volume:
docker compose -f docker-compose.yml run --rm --user root --entrypoint chown server -R 10001:10001 /app/uploads
(sostituire docker-compose.yml con docker-compose.dev.yml per
un'installazione di sviluppo/on-premise). Il comando usa il servizio
server perché è quello che monta il volume upload_data in lettura e
scrittura; il worker di visione lo monta in sola lettura e non ne ha
bisogno. Un'installazione nuova, che parte da un volume vuoto, non richiede
questo passo.
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 perAPP_DOMAIN, tramite ilcertresolverconfigurato nelle label del servizio (tls.certresolver=mytlschallenge). Serve solo cheAPP_DOMAINpunti 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, innginx/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