Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -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")