Stations were the headline V2.0.0 feature but had no user-facing
documentation outside the architecture page. Filled the gap across
the three operational docs.
USER_GUIDE.md
- New entries in "Key Concepts": Station and Station assignment.
- New "Recipes you see are filtered by station" subsection in the
MeasurementTec workflow, explaining why the Select Recipe page may
legitimately show fewer recipes than expected and what the
"Stazione non configurata" error means at the operator level.
- New "Station Management" section under Admin Workflow covering:
the mental model, station create/edit/delete, the two-column
recipe-assignment modal, the immutable-code rule, the role of the
ST-DEFAULT seed station, and the tablet deployment cheat sheet.
- Admin role description updated to mention stations.
DEPLOYMENT.md
- Environment Variables Reference: added STATION_CODE row and noted
that an empty value triggers the deliberate fail-fast HTTP 503 on
/measure/select. Updated RATE_LIMIT_GENERAL default (300, per the
V2.0.0 perf change). Clarified UPLOAD_DIR resolves against the
project root.
API.md
- New "Stations" endpoint section listing all eight routes with
request/response examples and the 401/403/404/409 error contract:
GET / POST /stations, GET /stations/{id}, PUT /stations/{id},
DELETE /stations/{id}, GET /stations/{id}/recipes,
GET /stations/by-code/{code}/recipes (the operator-facing one used
by the Flask client), POST /stations/{id}/recipes,
DELETE /stations/{id}/recipes/{recipe_id}.
- TOC updated with the new "Stations" anchor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 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) |
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
cd server
pip install -r requirements.txt
alembic upgrade head
This creates all required tables:
users- System usersrecipes- Measurement recipesrecipe_versions- Immutable recipe versionsrecipe_tasks- Tasks within recipesrecipe_subtasks- Subtasks within tasks (individual measurements)measurements- Recorded measurementsaccess_logs- API access audit trailsystem_settings- Configuration key-value pairsrecipe_version_audit- Recipe change history
3. Create Initial Admin User (Optional)
Use the Flask client to create the first admin user, or run:
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())
"
Server Deployment
Development Server
cd server
pip install -r requirements.txt
uvicorn 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 Server (Gunicorn + Uvicorn)
Install Gunicorn:
pip install gunicorn
Start server:
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:
pip install waitress
Start server:
cd server
waitress-serve --host=0.0.0.0 --port=8000 main:app
systemd Service (Linux)
Create /etc/systemd/system/tiemeasureflow-api.service:
[Unit]
Description=TieMeasureFlow API Server
After=network.target mysql.service
[Service]
Type=notify
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
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
Development Server
cd client
pip install -r requirements.txt
flask run --host 0.0.0.0 --port 5000
Runs at http://0.0.0.0:5000
Compile Tailwind CSS (Optional)
cd client
npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch
Production Server (Gunicorn)
pip install gunicorn
cd client
gunicorn --workers 4 --bind 0.0.0.0:5000 app:app
systemd Service (Linux)
Create /etc/systemd/system/tiemeasureflow-web.service:
[Unit]
Description=TieMeasureFlow Web Client
After=network.target tiemeasureflow-api.service
[Service]
Type=notify
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
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Production Deployment
Security Checklist
- Change all default passwords in
.env - Generate random
SERVER_SECRET_KEY(32+ characters) - 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
┌─────────────────┐
│ Nginx Proxy │ (Port 80/443)
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼──┐ ┌───▼──┐
│API │ │Web │
│:8000 │ │:5000 │
└──────┘ └──────┘
│ │
└────┬─────┘
│
┌────▼────┐
│ MySQL │
│ :3306 │
└─────────┘
Nginx Configuration
Create /etc/nginx/sites-available/tiemeasureflow:
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:
sudo ln -s /etc/nginx/sites-available/tiemeasureflow /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Docker Deployment
Docker Compose (Complete Stack)
The project includes a docker-compose.yml for easy deployment:
# Build images
docker-compose build
# Start services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
Manual Docker Build
API Server
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:
docker build -f server/Dockerfile -t tiemeasureflow-api .
docker run -p 8000:8000 --env-file .env tiemeasureflow-api
Web Client
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:
docker build -f client/Dockerfile -t tiemeasureflow-web .
docker run -p 5000:5000 --env-file .env tiemeasureflow-web
SSL/TLS
Self-Signed Certificate (Development)
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes
Let's Encrypt (Production)
sudo apt install certbot python3-certbot-nginx
sudo certbot certonly --nginx -d yourdomain.com
Configure in .env
SSL_CERTFILE=/etc/ssl/certs/yourdomain.com.crt
SSL_KEYFILE=/etc/ssl/private/yourdomain.com.key
Start Server with SSL
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
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
mysql -u tmflow -p -e "SELECT 1"
# Check .env variables
grep DB_ .env
# Test connection string
cd server && python -c "from config import settings; print(settings.database_url)"
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
uvicorn main:app --port 8001
Import Errors
ModuleNotFoundError: No module named 'fastapi'
Solution:
cd server
pip install -r requirements.txt
File Upload Not Working
/app/server/uploads: Permission denied
Solution:
# Create uploads directory with correct permissions
mkdir -p server/uploads
chmod 755 server/uploads
Migrations Failed
alembic.util.exc.CommandError: Can't locate revision identified by...
Solution:
cd server
# Check migration status
alembic current
# Reset migrations (CAUTION - deletes data)
alembic downgrade base
alembic 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