Deployment Architecture¶
Production Stack¶
flowchart LR
GitHub --> GHA[GitHub Actions\nCI/CD]
GHA --> Render[Render\nWeb Service]
Render --> Gunicorn[Gunicorn\n1 worker 6 threads]
Gunicorn --> Flask[Flask App]
Flask --> Postgres[(PostgreSQL\nRender Managed)]
Flask --> Wasabi[Wasabi S3\nAP Southeast-1]
Flask --> Redis[(Redis\nCelery broker)]
Render Configuration¶
| Setting | Value |
|---|---|
| Plan | Starter (512 MB RAM, shared CPU) |
| Region | Singapore (closest to India) |
| Start command | Procfile → web: gunicorn ... |
| DB | PostgreSQL (Render managed) |
| Auto-deploy | On push to main via GitHub Actions |
Gunicorn Configuration (Procfile)¶
web: gunicorn Lenzeye:app \
--workers=1 \
--threads=6 \
--timeout=120 \
--graceful-timeout=30 \
--keep-alive=5 \
--worker-class=gthread \
--worker-tmp-dir=/dev/shm \
--log-level=info \
--limit-request-line=4096 \
--limit-request-field_size=8190 \
--max-requests=1000 \
--max-requests-jitter=50
| Parameter | Value | Reason |
|---|---|---|
--workers=1 |
1 | Avoid RAM duplication across processes |
--threads=6 |
6 | Handle concurrent requests in one process |
--timeout=120 |
120s | Allow large encrypted uploads to complete |
--worker-class=gthread |
gthread | Thread-based, ideal for I/O-heavy workloads |
--worker-tmp-dir=/dev/shm |
RAM-backed | Fast worker heartbeat writes |
--max-requests=1000 |
1000 + jitter | Restart workers periodically to prevent memory drift |
Do not change --workers=1 without re-validating RAM
The validated production configuration runs 1 worker. Multiple workers each load the full app (models, blueprints, lazy imports) independently, doubling RAM usage. Peak RAM is already 398 MB on a 512 MB plan.
WSGI Entry Point¶
wsgi.py:
python
from Lenzeye import create_app
app = create_app()
Lenzeye.py contains create_app() which:
1. Creates Flask app instance
2. Loads environment variables
3. Configures SQLAlchemy with correct DB URL
4. Registers all 15+ blueprints
5. Initializes Celery, CORS, Migrate
Environment Separation¶
| Environment | DB | Config file |
|---|---|---|
| Local dev | SQLite (DSS_local.db) |
sendgrid.env |
| Production | PostgreSQL (Render) | render.env |
Detection: os.getenv("RENDER") — Render sets this automatically.
python
if os.getenv("RENDER"):
load_dotenv("render.env")
else:
load_dotenv("sendgrid.env")
Database Connection¶
python
engine_options = {
'pool_pre_ping': True, # Verify connections before use
'pool_recycle': 180, # Recycle connections every 3 min
'pool_timeout': 30, # Fail fast if pool exhausted
'connect_args': {'connect_timeout': 10} # PostgreSQL only
}
USE_INTERNAL_DB env var switches between Render internal and external DB URLs (internal is faster, no SSL overhead).
Wasabi S3 Configuration¶
python
s3 = boto3.client(
's3',
endpoint_url='https://s3.ap-southeast-1.wasabisys.com',
region_name='ap-southeast-1',
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=Config(
signature_version='s3v4',
max_pool_connections=50,
retries={'max_attempts': 3, 'mode': 'adaptive'},
connect_timeout=10,
read_timeout=60
)
)
TL;DR¶
Server: Render Starter plan, Singapore region. 1 Gunicorn worker, 6 gthread threads, 120s timeout.
DB: PostgreSQL (Render managed). SQLite locally.
Storage: Wasabi S3 AP Southeast-1, 50-connection pool, adaptive retry.
Deploy: GitHub push → GitHub Actions → Render auto-deploy.
Env separation: RENDER env var selects production config.