diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index eda23d4..4bdedd8 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -34,9 +34,13 @@ Caddy terminates TLS, so without it Django believes every request is plain HTTP: `header_up X-Forwarded-Proto`); don't remove either. **4. Uploads must move to object storage before the second app server.** -Club logos go to `MEDIA_ROOT` on local disk. On one box that is fine. On two, a logo -uploaded to node A is a 404 on node B. Setting `AWS_STORAGE_BUCKET_NAME` switches the -default storage to S3 — do it *before* you scale, not during. +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. **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.behind-proxy.yaml b/compose.behind-proxy.yaml index 47d4303..26fb2ad 100644 --- a/compose.behind-proxy.yaml +++ b/compose.behind-proxy.yaml @@ -19,6 +19,11 @@ services: env_file: .env.production ports: - "127.0.0.1:${WEB_PORT:-8001}:8000" + volumes: + # Uploaded club logos, while storage is local disk (see rosterchief/urls.py). Without + # this, a rebuild or recreate wipes MEDIA_ROOT even though the container itself keeps + # running fine in between. + - media_data:/app/media depends_on: db: condition: service_healthy @@ -53,3 +58,4 @@ services: volumes: pgdata: + media_data: diff --git a/compose.yaml b/compose.yaml index 53210c9..25d2d3c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -32,6 +32,11 @@ services: build: . restart: unless-stopped env_file: .env.production + volumes: + # Uploaded club logos, while storage is local disk (see rosterchief/urls.py). Without + # this, a rebuild or recreate wipes MEDIA_ROOT even though the container itself keeps + # running fine in between. + - media_data:/app/media depends_on: db: condition: service_healthy @@ -70,3 +75,4 @@ volumes: pgdata: caddy_data: caddy_config: + media_data: diff --git a/rosterchief/urls.py b/rosterchief/urls.py index b82a9a9..3d88e5a 100644 --- a/rosterchief/urls.py +++ b/rosterchief/urls.py @@ -39,5 +39,10 @@ if settings.DEBUG: if settings.BROWSER_RELOAD_AVAILABLE: urlpatterns += [path("__reload__/", include("django_browser_reload.urls"))] - # Club logos are uploads: runserver has to serve MEDIA_ROOT itself. +if not settings.AWS_STORAGE_BUCKET_NAME: + # Gated on the storage backend, not on DEBUG: local disk is the default until a bucket is + # configured (see settings.STORAGES), and Caddy only reverse-proxies — it never serves + # /media/* itself — so without this route every uploaded club logo 404s in production too. + # Once AWS_STORAGE_BUCKET_NAME is set, club.logo.url points straight at the bucket and this + # route is simply never hit. urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)