From fe7c99a07edc4425bb410291bfb5fc9b26cdbd7a Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Tue, 23 Jun 2026 09:32:27 -0700 Subject: [PATCH] Run Gunicorn on test/prod instead of Django's dev runserver (#1034) Django's `runserver` is a development server the docs explicitly warn against in production ("has not gone through security audits or performance tests"). We had been running it on both test and prod since 2017. Swap it for Gunicorn, the recommended WSGI server. The change is entirely inside the container: Apache still reverse-proxies dynamic requests to 127.0.0.1:8571 and serves /static/ and /media/ directly, so this ships via the normal push-to-deploy path with no Apache/UW CSE IT changes. - requirements.txt: add gunicorn==23.0.0 - docker-entrypoint.sh: start Gunicorn when DJANGO_ENV is TEST or PROD; keep `runserver` for local dev (DJANGO_ENV=DEBUG) so auto-reload, the debug toolbar, and DEBUG=True static serving still work. Workers (3) and timeout (120s) default conservatively for the shared host and are overridable via GUNICORN_WORKERS / GUNICORN_TIMEOUT. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-entrypoint.sh | 39 +++++++++++++++++++++++++++++++++++---- requirements.txt | 17 +++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 536b3cc9..b06254bd 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -164,8 +164,39 @@ python manage.py setup_admin_groups # python manage.py rename_talk_files # Start server -echo "Starting server" +# +# Production-grade environments (TEST, PROD) run Gunicorn, the recommended WSGI +# server. Local development (DJANGO_ENV=DEBUG) keeps Django's `runserver` for +# its auto-reload on code edits, friendlier tracebacks, debug toolbar, and +# static-file serving under DEBUG=True. See issue #1034. +# +# This swap is entirely inside the container -- UW CSE's Apache still reverse- +# proxies dynamic requests to 127.0.0.1:8571 (-> container :8000) and serves +# /static/ and /media/ directly, exactly as before -- so it ships via the +# normal push-to-deploy path with no Apache/IT changes. +# +# Gunicorn tuning (overridable via env vars in the compose file): +# GUNICORN_WORKERS number of worker processes. The (2*cores)+1 rule of thumb +# would be ~49 on the 24-core host, but that box is SHARED +# with all Project Sidewalk instances (see #959), so we +# default to a modest 3. +# GUNICORN_TIMEOUT per-request worker timeout in seconds. Gunicorn's default +# of 30s can kill slow admin operations (ImageMagick/PDF +# thumbnail generation), so we default to 120. echo "****************** STEP 5/5: docker-entrypoint.sh ************************" -echo "5. Starting server with 'python manage.py runserver 0.0.0.0:8000'" -echo "******************************************" -python manage.py runserver 0.0.0.0:8000 \ No newline at end of file +if [ "$DJANGO_ENV" = "TEST" ] || [ "$DJANGO_ENV" = "PROD" ]; then + GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}" + GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}" + echo "5. Starting Gunicorn (DJANGO_ENV=$DJANGO_ENV, workers=$GUNICORN_WORKERS, timeout=${GUNICORN_TIMEOUT}s)" + echo "******************************************" + exec gunicorn makeabilitylab.wsgi:application \ + --bind 0.0.0.0:8000 \ + --workers "$GUNICORN_WORKERS" \ + --timeout "$GUNICORN_TIMEOUT" \ + --access-logfile - \ + --error-logfile - +else + echo "5. Starting dev server with 'python manage.py runserver 0.0.0.0:8000' (DJANGO_ENV=$DJANGO_ENV)" + echo "******************************************" + exec python manage.py runserver 0.0.0.0:8000 +fi \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 337c609b..98dd810d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -116,6 +116,23 @@ django-sortedm2m==4.0.0 django-prose-editor[sanitize]==0.26.0 +# ----------------------------------------------------------------------------- +# WSGI Server (production) +# ----------------------------------------------------------------------------- +# Gunicorn is the production WSGI server. We previously ran Django's dev +# `runserver` on test AND prod, which the Django docs explicitly warn against +# ("DO NOT USE THIS SERVER IN A PRODUCTION SETTING ... has not gone through +# security audits or performance tests"). See issue #1034. +# +# Gunicorn runs inside the same container behind UW CSE's Apache reverse proxy +# (Apache still serves /static/ and /media/ directly and proxies dynamic +# requests to 127.0.0.1:8571 -> container :8000), so this swap is contained to +# the container and ships via the normal push-to-deploy path -- no Apache or +# UW CSE IT changes required. Worker count and request timeout are tunable via +# the GUNICORN_WORKERS / GUNICORN_TIMEOUT env vars in docker-entrypoint.sh. +# See: https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/gunicorn/ +gunicorn==23.0.0 + # ----------------------------------------------------------------------------- # Security & Networking # -----------------------------------------------------------------------------