From 4121f592c3f387fd0f0b8552db1c8b9d45fa6e72 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Tue, 12 May 2026 20:30:42 +0200 Subject: [PATCH] Replace gunicorn with waitress This commit replaces the `gunicorn` WSGI server with `waitress`. It loses the statsd statistics, but gives us peace of mind. The setup was tested by commenting out the `python manage.py run` command in `docker-compose.yml`. Note that migrations need to be run for the index page to work via `podman compose exec web -- python manage.py migrate`. Rationale ========= Over the last few months, our dependency `gunicorn` has received a large amount of contributions written primarily by the maintainer with close to zero external review. None of these implemented features are anything we need: dirty workers, uWSGI protocol support, control socket, shared memory, .... Nor do I ever recall looking for any of these features in it. The sheer volume of code and the general format of changes also strongly reeks of LLM authorship or assistance. Of course, every maintainer is free to do with their software whatever they wish. But, as with any other dependency, I will evaluate any updates, keeping in mind that any dependency we have here has full access to our database and API. We do not need a WSGI server with regular feature development. We need one that works and is maintained - that is the only requirement. See the linked issue for more discussion. Fixes: #1613 Signed-off-by: Johannes Christ --- Dockerfile | 6 ++---- gunicorn.conf.py | 10 ---------- manage.py | 2 +- pydis_site/README.md | 2 +- pyproject.toml | 3 +-- uv.lock | 34 +++++++++++----------------------- 6 files changed, 16 insertions(+), 41 deletions(-) delete mode 100644 gunicorn.conf.py diff --git a/Dockerfile b/Dockerfile index 28bb3af64..25230c346 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,5 @@ RUN if [ $STATIC_BUILD = "TRUE" ] ; \ then SECRET_KEY=dummy_value uv run python manage.py distill-local build --traceback --force ; \ fi -CMD ["gunicorn", "--preload", "-b", "0.0.0.0:8000", \ - "pydis_site.wsgi:application", "-w", "2", "--statsd-host", \ - "graphite.default.svc.cluster.local:8125", "--statsd-prefix", "site", \ - "--config", "file:gunicorn.conf.py"] +CMD ["waitress-serve", "--listen", "*:8000", \ + "--max-request-body-size=52428800", "pydis_site.wsgi:application"] diff --git a/gunicorn.conf.py b/gunicorn.conf.py deleted file mode 100644 index 4930ae5b8..000000000 --- a/gunicorn.conf.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Configuration file for gunicorn. - -Code taken from https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn -""" -from prometheus_client import multiprocess - - -def child_exit(server, worker) -> None: - multiprocess.mark_process_dead(worker.pid) diff --git a/manage.py b/manage.py index 38bf6be0e..4b72320dd 100755 --- a/manage.py +++ b/manage.py @@ -31,7 +31,7 @@ class SiteManager: Manages the preparation and serving of the website for local use. This class is used solely for setting up the development - environment. In production, gunicorn is invoked directly + environment. In production, the WSGI server is invoked directly and migrations are handled in an init container. Usage: diff --git a/pydis_site/README.md b/pydis_site/README.md index 7faa1593b..1ef24d222 100644 --- a/pydis_site/README.md +++ b/pydis_site/README.md @@ -53,7 +53,7 @@ the website: determine _which URLs will lead to which Django views_. - [`wsgi.py`](./wsgi.py), which serves as an adapter for - [`gunicorn`](https://github.com/benoitc/gunicorn), + [`waitress`](https://github.com/Pylons/waitress), [`uwsgi`](https://github.com/unbit/uwsgi), or other application servers to run our application in production. Unless you want to test an interaction between our application and those servers, you probably won't need to touch this. diff --git a/pyproject.toml b/pyproject.toml index ba86b9cd8..ef4bf3dae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "django-prometheus==2.4.1", "django-simple-bulma==2.6.0", "djangorestframework==3.16.0", - "gunicorn==24.1.1", "httpx==0.28.1", "markdown==3.8.2", "psycopg[binary]==3.2.9", @@ -22,6 +21,7 @@ dependencies = [ "python-frontmatter==1.1.0", "pyyaml==6.0.2", "sentry-sdk==2.33.0", + "waitress==3.0.2", "whitenoise==6.9.0", ] @@ -76,7 +76,6 @@ select = ["ANN", "B", "C4", "D", "DJ", "DTZ", "E", "F", "ISC", "INT", "N", "PGH" "pydis_site/apps/**/tests/test_*.py" = ["ANN", "D"] "static-builds/netlify_build.py" = ["T201"] "pydis_site/apps/api/tests/test_off_topic_channel_names.py" = ["RUF001"] -"gunicorn.conf.py" = ["ANN", "D"] "pydis_site/apps/api/models/bot/off_topic_channel_name.py" = ["RUF001"] [tool.taskipy.tasks] diff --git a/uv.lock b/uv.lock index 92f7b2e88..6ca425628 100644 --- a/uv.lock +++ b/uv.lock @@ -267,18 +267,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] -[[package]] -name = "gunicorn" -version = "24.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/0a/10739c03537ec5b131a867bf94df2e412b437696c7e5d26970e2198a80d2/gunicorn-24.1.1.tar.gz", hash = "sha256:f006d110e5cb3102859b4f5cd48335dbd9cc28d0d27cd24ddbdafa6c60929408", size = 287567, upload-time = "2026-01-24T01:15:31.359Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/90/cfe637677916fc6f53cd2b05d5746e249f683e1fa14c9e745a88c66f7290/gunicorn-24.1.1-py3-none-any.whl", hash = "sha256:757f6b621fc4f7581a90600b2cd9df593461f06a41d7259cb9b94499dc4095a8", size = 114920, upload-time = "2026-01-24T01:15:29.656Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -374,15 +362,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - [[package]] name = "platformdirs" version = "4.3.8" @@ -606,7 +585,6 @@ dependencies = [ { name = "django-prometheus" }, { name = "django-simple-bulma" }, { name = "djangorestframework" }, - { name = "gunicorn" }, { name = "httpx" }, { name = "markdown" }, { name = "psycopg", extra = ["binary"] }, @@ -615,6 +593,7 @@ dependencies = [ { name = "python-frontmatter" }, { name = "pyyaml" }, { name = "sentry-sdk" }, + { name = "waitress" }, { name = "whitenoise" }, ] @@ -640,7 +619,6 @@ requires-dist = [ { name = "django-prometheus", specifier = "==2.4.1" }, { name = "django-simple-bulma", specifier = "==2.6.0" }, { name = "djangorestframework", specifier = "==3.16.0" }, - { name = "gunicorn", specifier = "==24.1.1" }, { name = "httpx", specifier = "==0.28.1" }, { name = "markdown", specifier = "==3.8.2" }, { name = "psycopg", extras = ["binary"], specifier = "==3.2.9" }, @@ -649,6 +627,7 @@ requires-dist = [ { name = "python-frontmatter", specifier = "==1.1.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "sentry-sdk", specifier = "==2.33.0" }, + { name = "waitress", specifier = "==3.0.2" }, { name = "whitenoise", specifier = "==6.9.0" }, ] @@ -745,6 +724,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] +[[package]] +name = "waitress" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901, upload-time = "2024-11-16T20:02:35.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, +] + [[package]] name = "whitenoise" version = "6.9.0"