Chapter 3: Django – Collect Static Files
Collecting static files — the python manage.py collectstatic command.
This is the step that separates “it works on my laptop” from “it works on the internet”.
Many people reach deployment day, see 404 errors on CSS/JS/images/favicons, panic, and spend hours googling “Django static files not working Railway / Render / Heroku”.
Today I’m going to teach you exactly what collectstatic does, why you need it, how to run it properly, what happens behind the scenes, common traps, and how to test it so you never get surprised during real deployment.
We’ll do this very slowly, step-by-step, like pair-programming — I’ll explain every command, every setting, every folder, and we’ll test everything together.
Step 1 – Why Do We Need collectstatic At All?
In development (runserver):
- Django automatically finds and serves static files from:
- each app’s static/ folder
- folders in STATICFILES_DIRS
- No problem — magic dev server handles everything
In production (real web server):
- Django does NOT serve static files (security + performance reasons)
- A separate web server (nginx, Apache) or middleware (WhiteNoise) must serve them
- All static files from every app + project folders must be copied into one single folder
- That single folder is called STATIC_ROOT
- collectstatic is the command that copies everything into STATIC_ROOT
Without collectstatic → production server sees 404 on every CSS/JS/image → site looks broken
Step 2 – Confirm Your Static Files Settings (Very Important)
Open mysite/settings.py
Make sure these lines exist (add them if missing):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent # ── Static files settings ──────────────────────────────────────────────────── STATIC_URL = '/static/' # URL prefix in browser # Where Django looks for static files during development STATICFILES_DIRS = [ BASE_DIR / 'static', # project-level static folder # You can add more folders here if needed ] # Where collectstatic copies everything (production) STATIC_ROOT = BASE_DIR / 'staticfiles' # ← never put files here manually! # Recommended storage for compression & cache-busting (with WhiteNoise) STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' |
Important notes:
- STATICFILES_DIRS → extra places Django searches in development
- STATIC_ROOT → empty folder that collectstatic will fill
- Never manually put files in STATIC_ROOT — collectstatic overwrites it
Step 3 – Create Some Static Files (If You Haven’t Already)
Quick example structure:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mysite-project/ ├── static/ ← project-level static │ ├── css/ │ │ └── global.css │ ├── js/ │ │ └── alert.js │ └── images/ │ └── logo.png ├── polls/ │ └── static/ │ └── polls/ │ ├── css/ │ │ └── polls.css │ └── images/ │ └── poll-icon.png |
If you don’t have files yet, create minimal ones:
static/css/global.css:
|
0 1 2 3 4 5 6 7 |
body { font-family: system-ui; background: #f8f9fa; } .btn { background: #006400; color: white; padding: 10px 20px; border-radius: 6px; } |
static/images/logo.png → any small image (or placeholder text PNG)
Step 4 – Run collectstatic for the First Time
In terminal (project root):
|
0 1 2 3 4 5 6 |
python manage.py collectstatic |
You will see:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
You have requested to collect static files at the following destination: /path/to/your/project/staticfiles Are you sure you want to do this? Type 'yes' to continue, or 'no' to cancel: |
Type yes and press Enter.
Then:
|
0 1 2 3 4 5 6 |
X static files copied to '/path/to/your/project/staticfiles'. |
What just happened?
- Django searched:
- STATICFILES_DIRS (static/)
- every installed app’s static/ folder (polls/static/, pages/static/, etc.)
- Copied all files into staticfiles/
- Used the storage backend (CompressedManifestStaticFilesStorage if you set it) → added content hashes to filenames (cache busting) → compressed files (gzip/brotli)
Now look inside staticfiles/:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
staticfiles/ ├── css/ │ └── global.css ├── polls/ │ └── css/ │ └── polls.css └── images/ └── logo.png |
→ All files are now in one place — ready for production server.
Step 5 – Test in Development (WhiteNoise Serves Them)
If you have WhiteNoise configured (middleware + storage):
Restart server:
|
0 1 2 3 4 5 6 |
python manage.py runserver |
Visit /polls/ or any page:
- Logo appears
- Global CSS applied
- App-specific CSS applied
- Browser dev tools → files served from /static/… with 200 OK
WhiteNoise serves from STATIC_ROOT even in development — very realistic test.
Step 6 – Production Deployment Flow (When You’re Ready)
- Set DEBUG = False
- Set ALLOWED_HOSTS = [‘yourdomain.com’, ‘.railway.app’, …]
- Run python manage.py collectstatic –noinput (in deploy script / pipeline)
- Use a platform that supports Python + persistent filesystem:
- Railway / Render / Fly.io → automatic
- Heroku → needs Whitenoise + buildpack
- VPS → nginx serves /static/ → /path/to/staticfiles/
WhiteNoise middleware handles serving automatically.
Step 7 – Common “Why 404?” Traps & Fixes
| Problem | Cause | Fix |
|---|---|---|
| 404 on static files after deploy | Forgot to run collectstatic | Run it every deploy |
| Files missing from staticfiles/ | Forgot STATICFILES_DIRS or app static/ folder | Check settings & folder structure |
| Old hashes / no cache busting | Not using CompressedManifestStaticFilesStorage | Set STATICFILES_STORAGE |
| CSS/JS not applying | Forgot {% load static %} in template | Add at top of every template that uses static |
| collectstatic asks for confirmation | Normal first time | Use –noinput in scripts |
Your Quick Practice Task (Do This Right Now)
-
Confirm STATICFILES_STORAGE and STATIC_ROOT in settings.py
-
Create or verify at least one CSS file in static/css/ and one in polls/static/polls/css/
-
Run:
Bash0123456python manage.py collectstatic -
Look inside staticfiles/ → see all files copied
-
Restart server → check if styles/images load
-
(Optional) Add a favicon:
static/images/favicon.ico → <link rel=”icon” href=”{% static ‘images/favicon.ico’ %}”>
Tell me what feels next:
- “Done! Now show me how to deploy to Railway / Render with WhiteNoise”
- “How to handle user-uploaded images (MEDIA files) in production?”
- “What is the difference between WhiteNoise and serving via nginx?”
- “Got error during collectstatic – here’s message”
- Or finally ready for Django Forms + Voting + POST + F() + results page
You now understand collectstatic completely — this one command is the difference between “broken site on internet” and “beautiful working app”.
You’re doing really well — let’s keep building! 🚀🇮🇳
