feat(cli): use gunicorn to manage server workers on unix systems

Implement Gunicorn + Uvicorn deployment strategy for Unix systems to provide
multi-process parallelism and high-concurrency async request handling.

Key Features:
- Platform detection: Uses Gunicorn on Unix (Linux/macOS), falls back to
  Uvicorn on Windows
- Worker management: Auto-calculates workers as (2 * CPU cores) + 1 with
  env var overrides (GUNICORN_WORKERS, WEB_CONCURRENCY)
- Production optimizations:
  * Worker recycling (--max-requests, --max-requests-jitter) prevents memory leaks
  * Configurable worker connections (default: 1000 per worker)
  * Connection keepalive for improved performance
  * Automatic log level mapping from Python logging to Gunicorn
  * Optional --preload for memory efficiency (disabled by default)
- IPv6 support: Proper bind address formatting for IPv6 addresses
- SSL/TLS: Passes through certificate configuration from uvicorn_config
- Comprehensive logging: Reports workers, capacity, and configuration details
- Graceful fallback: Falls back to Uvicorn if Gunicorn not installed

Configuration via Environment Variables:
- GUNICORN_WORKERS / WEB_CONCURRENCY: Override worker count
- GUNICORN_WORKER_CONNECTIONS: Concurrent connections per worker
- GUNICORN_TIMEOUT: Worker timeout (default: 120s for async workers)
- GUNICORN_KEEPALIVE: Connection keepalive (default: 5s)
- GUNICORN_MAX_REQUESTS: Worker recycling interval (default: 10000)
- GUNICORN_MAX_REQUESTS_JITTER: Randomize restart timing (default: 1000)
- GUNICORN_PRELOAD: Enable app preloading for production (default: false)

Based on best practices from:
- DeepWiki analysis of encode/uvicorn and benoitc/gunicorn repositories
- Medium article: "Mastering Gunicorn and Uvicorn: The Right Way to Deploy
  FastAPI Applications"

Fixes:
- Avoids worker multiplication anti-pattern (nested workers)
- Proper IPv6 bind address formatting ([::]:port)
- Correct Gunicorn parameter names (--keep-alive vs --keepalive)

Dependencies:
- Added gunicorn>=23.0.0 to pyproject.toml

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Roy Belio 2025-10-29 17:09:17 +02:00
parent e809d21357
commit e72583cd9c
6 changed files with 198 additions and 1 deletions

14
uv.lock generated
View file

@ -1409,6 +1409,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/96/44759eca966720d0f3e1b105c43f8ad4590c97bf8eb3cd489656e9590baa/grpcio-1.67.1-cp313-cp313-win_amd64.whl", hash = "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba", size = 4346042, upload-time = "2024-10-29T06:25:21.939Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@ -1941,6 +1953,7 @@ dependencies = [
{ name = "asyncpg" },
{ name = "fastapi" },
{ name = "fire" },
{ name = "gunicorn" },
{ name = "h11" },
{ name = "httpx" },
{ name = "jinja2" },
@ -2092,6 +2105,7 @@ requires-dist = [
{ name = "asyncpg" },
{ name = "fastapi", specifier = ">=0.115.0,<1.0" },
{ name = "fire" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "h11", specifier = ">=0.16.0" },
{ name = "httpx" },
{ name = "jinja2", specifier = ">=3.1.6" },