diff --git a/Dockerfile b/Dockerfile index b33b384..f90e39c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,6 +43,7 @@ COPY schema_mssql.sql ./ COPY app.py . COPY config.py . COPY dash_app.py . +COPY gunicorn.conf.py . COPY assets/ ./assets/ COPY static/ ./static/ COPY pages/ ./pages/ @@ -59,9 +60,11 @@ RUN groupadd --gid 10001 app && \ # Expose single internal app port; external ports are mapped via docker-compose EXPOSE 5000 -# Run the app +# Run the app with a production-grade WSGI server. Gunicorn keeps a master +# process alive to replace failed workers and does not run Flask's debug +# reloader, so application workers cannot silently reload inside the container. USER app -CMD ["python", "app.py"] +CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:server"] # Dev-only stage: add Microsoft ODBC Driver for SQL Server (local dev container) FROM base AS dev diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..2af3b0d --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,30 @@ +"""Gunicorn configuration for the HapSearch container.""" + +import os + + +bind = f"{os.getenv('APP_HOST', '0.0.0.0')}:{os.getenv('APP_PORT', '5000')}" + +# Multiple threaded workers keep the site available when a request is slow or +# a worker exits. Every value can be tuned per environment without rebuilding. +workers = int(os.getenv("GUNICORN_WORKERS", "2")) +worker_class = "gthread" +threads = int(os.getenv("GUNICORN_THREADS", "4")) +timeout = int(os.getenv("GUNICORN_TIMEOUT", "120")) +graceful_timeout = int(os.getenv("GUNICORN_GRACEFUL_TIMEOUT", "30")) +keepalive = int(os.getenv("GUNICORN_KEEPALIVE", "5")) + +# Periodically replace workers to limit the impact of gradual memory growth. +# Jitter prevents all workers from being recycled at the same time. +max_requests = int(os.getenv("GUNICORN_MAX_REQUESTS", "2000")) +max_requests_jitter = int(os.getenv("GUNICORN_MAX_REQUESTS_JITTER", "200")) + +accesslog = "-" +errorlog = "-" +capture_output = True + +# The existing load balancer connects to this backend over HTTPS. Local +# environments can disable TLS by leaving TLS_ENABLED unset or false. +if os.getenv("TLS_ENABLED", "false").lower() in {"true", "1", "yes"}: + certfile = os.getenv("TLS_CERT_PATH", "/cert.pem") + keyfile = os.getenv("TLS_KEY_PATH", "/key.pem")