From 1be9959481bf8de1ba1d98eaea9a941d5b71505d Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Thu, 6 Aug 2026 22:18:27 +0200 Subject: [PATCH] Serve club logos directly from Caddy instead of Django MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every image request was round-tripping through a gunicorn worker for what is just a static file on disk. Caddy now serves /media/* straight off the shared media_data volume (mounted read-only) and only falls through to Django for anything else — Django's own /media/* route stays as a fallback for compose.behind-proxy.yaml and runserver, where there is no bundled Caddy container. --- DEPLOYMENT.md | 14 ++++++++------ compose.yaml | 3 +++ deploy/caddy/Caddyfile | 9 +++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4bdedd8..edba93b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -35,12 +35,14 @@ Caddy terminates TLS, so without it Django believes every request is plain HTTP: **4. Uploads must move to object storage before the second app server.** Club logos go to `MEDIA_ROOT` on local disk by default. `compose.yaml` mounts a `media_data` -volume so that survives a rebuild, and `rosterchief/urls.py` serves `/media/*` itself whenever -`AWS_STORAGE_BUCKET_NAME` is unset — Caddy only reverse-proxies, it never serves media on its -own, so without that route every logo 404s even on one box. On two boxes local disk stops -working regardless: a logo uploaded to node A is still a 404 on node B, since nothing shares -the volume between them. Setting `AWS_STORAGE_BUCKET_NAME` switches the default storage to S3 -— do it *before* you scale, not during. +volume, shared read-write with `web` and read-only with `caddy`, so uploads both survive a +rebuild and get served by Caddy directly (`handle_path /media/*` in the Caddyfile) rather than +round-tripping through a gunicorn worker. `rosterchief/urls.py` still serves `/media/*` itself +as a fallback whenever `AWS_STORAGE_BUCKET_NAME` is unset — needed for `compose.behind-proxy.yaml` +(no bundled Caddy there) and for `runserver`. On two boxes local disk stops working regardless +of any of this: a logo uploaded to node A is still a 404 on node B, since nothing shares the +volume between them. Setting `AWS_STORAGE_BUCKET_NAME` switches the default storage to S3 — do +it *before* you scale, not during. **5. PDF invoices need native libraries.** WeasyPrint binds to pango/cairo. The image installs them; a bare-metal deploy would need diff --git a/compose.yaml b/compose.yaml index 25d2d3c..7969e53 100644 --- a/compose.yaml +++ b/compose.yaml @@ -25,6 +25,9 @@ services: - ./deploy/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config + # Read-only: Caddy serves club logos straight off disk instead of round-tripping every + # image request through a gunicorn worker. Same volume `web` writes uploads into. + - media_data:/srv/media:ro depends_on: - web diff --git a/deploy/caddy/Caddyfile b/deploy/caddy/Caddyfile index dd1c219..a4d4a53 100644 --- a/deploy/caddy/Caddyfile +++ b/deploy/caddy/Caddyfile @@ -10,6 +10,15 @@ encode zstd gzip + # Club logos, served straight off the shared volume — no gunicorn worker involved. Only + # matters while storage is local disk; once AWS_STORAGE_BUCKET_NAME is set, club.logo.url + # points at the bucket directly and this block simply never matches. A missing file 404s + # here exactly as django.views.static.serve would, so there is no need to fall through. + handle_path /media/* { + root * /srv/media + file_server + } + # X-Forwarded-Proto is what SECURE_PROXY_SSL_HEADER reads. Without it Django believes every # request is plain HTTP: request.is_secure() goes false, WebAuthn disagrees with the browser # about the origin, and the SSL redirect becomes a loop.