Subtitle: Or, how I lost my patience debugging staticfiles in production and came out stronger for it.
So you want Django REST Framework in the back, React in the front, and everything neatly dockerized like a perfect startup pitch deck. Great. But once you drop Gunicorn, Nginx, and Vite into the pot, what you get is less a gourmet app stack and more a soup of HTTP 400s, 403s, and 404s.
I recently went through the joyless gauntlet of getting Django admin to load properly behind a React/Nginx frontend in Docker. Here’s what worked — and all the dumb mistakes I made along the way so you don’t have to.
🔧 The Setup (a.k.a. “This Should Just Work, Right?”)
Here's what I started with:
-
Django
backend with Django REST Framework andgunicorn
-
React
frontend built with Vite -
Docker Compose
for everything -
Nginx
acting as the single entry point in production - Static and media volumes shared between Django and Nginx
My Docker Compose looked like this:
services:
web:
build:
context: ./magsite_backend
dockerfile: Dockerfile.prod
command: gunicorn magsite_backend.wsgi:application --bind 0.0.0.0:8000
expose:
- 8000
volumes:
- static_volume:/home/magsite_backend/web/staticfiles
- media_volume:/home/magsite_backend/web/mediafiles
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:16
volumes:
- prod_postgres_data:/var/lib/postgresql/data/
env_file:
- .env.prod.db
frontend:
build:
context: ./magsite-ui
dockerfile: Dockerfile.prod
ports:
- 80:80
depends_on:
- web
volumes:
- static_volume:/home/magsite_backend/web/staticfiles
- media_volume:/home/magsite_backend/web/mediafiles
volumes:
static_volume:
media_volume:
prod_postgres_data:
🪵 Step 1: Static Files Are Real, But Nobody Believes in Them
Everything built. Containers were up. Admin page loaded.
But CSS? Not so much. The page looked like early 2000s Craigslist.
Diagnosis: Django’s collectstatic
was dumping files into STATIC_ROOT
, but Nginx wasn’t serving them.
The screw-up: I had set:
STATICFILES_DIRS = [BASE_DIR / "staticfiles"]
STATIC_ROOT = BASE_DIR / "staticfiles"
Classic rookie move. Django won’t let you collect static files into the same place it reads them from.
The fix: Nuke STATICFILES_DIRS
entirely unless you actually have extra static folders outside your apps. Then:
STATIC_ROOT = BASE_DIR / "staticfiles"
🚧 Step 2: Nginx Serves Nothing
My Nginx config had this gem:
location /static/ {
proxy_pass http://web:8000/static/;
}
That works great in dev. In production? Gunicorn doesn’t serve static files. It’s a web server, not your butler.
The fix: Replace that with a direct file alias:
location /static/ {
alias /home/magsite_backend/web/staticfiles/;
}
And while you're at it, do the same for /media/
.
🔐 Step 3: Hello Darkness My Old Friend (403 CSRF Errors)
So now the admin loads and I try logging in — only to be met by Django’s infamous CSRF slap.
The issue: Nginx wasn't forwarding all the headers Django expects.
The fix: Update /admin/
and /api/
proxy blocks:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Bonus fix: In settings.py
, trust your reverse proxy:
CSRF_TRUSTED_ORIGINS = ['http://localhost']
🧱 Step 4: Bad Request (400) — Django’s Favorite Passive Aggression
Turns out even if your containers are hugging each other lovingly over an internal Docker network, Django still throws a tantrum if the Host
header doesn’t match what's in ALLOWED_HOSTS
.
The fix:
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
Or, if you’re still debugging:
ALLOWED_HOSTS = ['*']
Yes, it’s insecure. So is walking barefoot into prod. You’ll put shoes on later.
✅ Recap: Make It Work Every Time
Your key config checkpoints:
- ✅
STATIC_ROOT
is not inSTATICFILES_DIRS
- ✅
collectstatic
writes to a volume that Nginx can access - ✅ Nginx uses
alias
for static/media, notproxy_pass
- ✅ Nginx forwards critical headers (
Host
,X-Forwarded-*
) - ✅
ALLOWED_HOSTS
includes what’s actually hitting Django - ✅
CSRF_TRUSTED_ORIGINS
includes your Nginx URL - ✅ Browser cache cleared (or just use Incognito)
Final Thoughts
Django and React can co-exist in Docker with Nginx, but they do so reluctantly. Like British roommates who share a flat but text each other from separate rooms.
If you're going this route for your own project or your boss's next pie-in-the-sky full-stack SPA, expect to spend some time unraveling these little devops lies.
You're welcome.
Tags:
#django
#react
#docker
#nginx
#webdev
#csrf
#fullstack