168 lines
7.1 KiB
Python
168 lines
7.1 KiB
Python
# Gunicorn Configuration File for Trasabilitate Application
|
|
# Docker-optimized Production WSGI server configuration
|
|
|
|
import multiprocessing
|
|
import os
|
|
|
|
# ============================================================================
|
|
# SERVER SOCKET CONFIGURATION
|
|
# ============================================================================
|
|
# Bind to all interfaces on port from environment or default
|
|
bind = os.getenv("GUNICORN_BIND", "0.0.0.0:8781")
|
|
backlog = int(os.getenv("GUNICORN_BACKLOG", "2048"))
|
|
|
|
# ============================================================================
|
|
# WORKER PROCESSES CONFIGURATION
|
|
# ============================================================================
|
|
# Calculate workers: For Docker, use CPU count * 2 + 1 (but allow override)
|
|
# In Docker, cpu_count() returns container CPU limit if set
|
|
workers = int(os.getenv("GUNICORN_WORKERS", multiprocessing.cpu_count() * 2 + 1))
|
|
|
|
# Worker class - 'sync' is stable for most use cases
|
|
# Alternative: 'gevent' or 'gthread' for better concurrency
|
|
worker_class = os.getenv("GUNICORN_WORKER_CLASS", "sync")
|
|
|
|
# Max simultaneous connections per worker
|
|
worker_connections = int(os.getenv("GUNICORN_WORKER_CONNECTIONS", "1000"))
|
|
|
|
# Workers silent for more than this many seconds are killed and restarted
|
|
# Increase for long-running requests (file uploads, reports, large backups)
|
|
# For 5GB+ database operations, allow up to 30 minutes
|
|
timeout = int(os.getenv("GUNICORN_TIMEOUT", "1800")) # 30 minutes
|
|
|
|
# Keep-alive for reusing connections
|
|
keepalive = int(os.getenv("GUNICORN_KEEPALIVE", "5"))
|
|
|
|
# Graceful timeout - time to wait for workers to finish during shutdown
|
|
graceful_timeout = int(os.getenv("GUNICORN_GRACEFUL_TIMEOUT", "30"))
|
|
|
|
# ============================================================================
|
|
# WORKER LIFECYCLE - PREVENT MEMORY LEAKS
|
|
# ============================================================================
|
|
# Restart workers after this many requests to prevent memory leaks
|
|
max_requests = int(os.getenv("GUNICORN_MAX_REQUESTS", "1000"))
|
|
max_requests_jitter = int(os.getenv("GUNICORN_MAX_REQUESTS_JITTER", "100"))
|
|
|
|
# ============================================================================
|
|
# LOGGING CONFIGURATION
|
|
# ============================================================================
|
|
# Docker-friendly: logs to stdout/stderr by default, but allow file logging
|
|
# Automatically detect the correct log path based on current directory
|
|
default_log_dir = "/srv/quality_app/logs" if "/srv/quality_app" in os.getcwd() else "/srv/quality_recticel/logs"
|
|
accesslog = os.getenv("GUNICORN_ACCESS_LOG", f"{default_log_dir}/access.log")
|
|
errorlog = os.getenv("GUNICORN_ERROR_LOG", f"{default_log_dir}/error.log")
|
|
|
|
# For pure Docker logging (12-factor app), use:
|
|
# accesslog = "-" # stdout
|
|
# errorlog = "-" # stderr
|
|
|
|
loglevel = os.getenv("GUNICORN_LOG_LEVEL", "info")
|
|
|
|
# Enhanced access log format with timing and user agent
|
|
access_log_format = (
|
|
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s '
|
|
'"%(f)s" "%(a)s" %(D)s µs'
|
|
)
|
|
|
|
# Capture stdout/stderr in log (useful for print statements)
|
|
capture_output = os.getenv("GUNICORN_CAPTURE_OUTPUT", "true").lower() == "true"
|
|
|
|
# ============================================================================
|
|
# PROCESS NAMING & DAEMON
|
|
# ============================================================================
|
|
proc_name = os.getenv("GUNICORN_PROC_NAME", "trasabilitate_app")
|
|
|
|
# CRITICAL FOR DOCKER: Never use daemon mode in containers
|
|
# Docker needs the process to run in foreground
|
|
daemon = False
|
|
|
|
# ============================================================================
|
|
# SECURITY & LIMITS
|
|
# ============================================================================
|
|
# Request line size limit (protect against large headers)
|
|
limit_request_line = int(os.getenv("GUNICORN_LIMIT_REQUEST_LINE", "4094"))
|
|
limit_request_fields = int(os.getenv("GUNICORN_LIMIT_REQUEST_FIELDS", "100"))
|
|
limit_request_field_size = int(os.getenv("GUNICORN_LIMIT_REQUEST_FIELD_SIZE", "8190"))
|
|
|
|
# ============================================================================
|
|
# PERFORMANCE OPTIMIZATION
|
|
# ============================================================================
|
|
# Preload application before forking workers
|
|
# Pros: Faster worker spawn, less memory if using copy-on-write
|
|
# Cons: Code changes require full restart
|
|
preload_app = os.getenv("GUNICORN_PRELOAD_APP", "true").lower() == "true"
|
|
|
|
# Pseudo-random number for load balancing
|
|
worker_tmp_dir = os.getenv("GUNICORN_WORKER_TMP_DIR", "/dev/shm")
|
|
|
|
# ============================================================================
|
|
# SSL CONFIGURATION (if needed)
|
|
# ============================================================================
|
|
# Uncomment and set environment variables if using HTTPS
|
|
# keyfile = os.getenv("SSL_KEY_FILE")
|
|
# certfile = os.getenv("SSL_CERT_FILE")
|
|
# ca_certs = os.getenv("SSL_CA_CERTS")
|
|
|
|
# ============================================================================
|
|
# SERVER HOOKS - LIFECYCLE CALLBACKS
|
|
# ============================================================================
|
|
|
|
def on_starting(server):
|
|
"""Called just before the master process is initialized."""
|
|
server.log.info("=" * 60)
|
|
server.log.info("🚀 Trasabilitate Application - Starting Server")
|
|
server.log.info("=" * 60)
|
|
server.log.info("📍 Configuration:")
|
|
server.log.info(f" • Workers: {workers}")
|
|
server.log.info(f" • Worker Class: {worker_class}")
|
|
server.log.info(f" • Timeout: {timeout}s")
|
|
server.log.info(f" • Bind: {bind}")
|
|
server.log.info(f" • Preload App: {preload_app}")
|
|
server.log.info(f" • Max Requests: {max_requests} (+/- {max_requests_jitter})")
|
|
server.log.info("=" * 60)
|
|
|
|
|
|
def when_ready(server):
|
|
"""Called just after the server is started."""
|
|
server.log.info("=" * 60)
|
|
server.log.info("✅ Trasabilitate Application Server is READY!")
|
|
server.log.info(f"📡 Listening on: {server.address}")
|
|
server.log.info(f"🌐 Access the application at: http://{bind}")
|
|
server.log.info("=" * 60)
|
|
|
|
|
|
def on_exit(server):
|
|
"""Called just before exiting Gunicorn."""
|
|
server.log.info("=" * 60)
|
|
server.log.info("👋 Trasabilitate Application - Shutting Down")
|
|
server.log.info("=" * 60)
|
|
|
|
|
|
def worker_int(worker):
|
|
"""Called just after a worker exited on SIGINT or SIGQUIT."""
|
|
worker.log.info("⚠️ Worker %s received INT or QUIT signal", worker.pid)
|
|
|
|
|
|
def pre_fork(server, worker):
|
|
"""Called just before a worker is forked."""
|
|
server.log.info("🔄 Forking new worker (pid: %s)", worker.pid)
|
|
|
|
|
|
def post_fork(server, worker):
|
|
"""Called just after a worker has been forked."""
|
|
server.log.info("✨ Worker spawned successfully (pid: %s)", worker.pid)
|
|
|
|
|
|
def pre_exec(server):
|
|
"""Called just before a new master process is forked."""
|
|
server.log.info("🔄 Master process forking...")
|
|
|
|
|
|
def worker_abort(worker):
|
|
"""Called when a worker received the SIGABRT signal."""
|
|
worker.log.warning("🚨 Worker %s received SIGABRT signal - ABORTING!", worker.pid)
|
|
|
|
|
|
def child_exit(server, worker):
|
|
"""Called just after a worker has been exited, in the master process."""
|
|
server.log.info("👋 Worker %s exited", worker.pid) |