Yesterday, the API was fully secured with token authentication. Today, for Day 72, I covered the practical side of Django that nobody talks about much, but every real project needs static files, media files, and environment variables. These aren't glamorous topics, but skipping them means your project isn't ready for deployment.
Static Files
Static files are files that don't change: CSS, JavaScript, and images that are part of your design. They're called "static" because they're served as-is, no processing involved.
Configuration
In settings.py:
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATIC_ROOT = BASE_DIR / 'staticfiles'
-
STATIC_URL— the URL prefix for static files in the browser -
STATICFILES_DIRS— where Django looks for static files during development -
STATIC_ROOT— where Django collects all static files for production
Folder Structure
Create a static folder at the project root:
mysite/
static/
css/
style.css
js/
main.js
images/
logo.png
core/
mysite/
manage.py
You can also keep static files inside each app:
core/
static/
core/
css/
style.css
The app-level folder follows the same namespacing pattern as templates; the inner core/ folder prevents naming conflicts between apps.
Using Static Files in Templates
{% load static %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<img src="{% static 'images/logo.png' %}" alt="Logo">
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>
{% load static %} must be at the top of any template that uses static files. {% static 'path/to/file' %} generates the correct URL.
Collecting Static Files for Production
In production, Django doesn't serve static files itself; a web server like Nginx does. Before deploying, run:
python manage.py collectstatic
This copies every static file from all your apps and STATICFILES_DIRS into STATIC_ROOT. Your web server then serves files from that single folder.
Media Files
Media files are files uploaded by users: profile pictures, post images, attachments. Unlike static files, these are dynamic and generated at runtime.
Configuration
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
-
MEDIA_URL— URL prefix for uploaded files -
MEDIA_ROOT— where uploaded files are stored on disk
Serving Media Files in Development
Django doesn't serve media files automatically. Add this to your project's urls.py:
# mysite/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
This only works in development. In production, your web server handles media files.
Adding a File Upload Field to a Model
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
image = models.ImageField(upload_to='posts/', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
upload_to='posts/' means uploaded images go into media/posts/. Run migrations after adding this field.
ImageField requires the Pillow package:
pip install Pillow
Handling File Uploads in a Form
When a form includes file uploads, the view needs request.FILES and the HTML form needs enctype:
def create_post(request):
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect('home')
else:
form = PostForm()
return render(request, 'core/create_post.html', {'form': form})
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
Without enctype="multipart/form-data", file data is not included in the POST request.
Displaying Uploaded Images in Templates
{% if post.image %}
<img src="{{ post.image.url }}" alt="{{ post.title }}">
{% endif %}
Always check if the image exists before rendering; the field is optional here.
Environment Variables
Right now, settings.py likely has the secret key hardcoded, DEBUG=True, and database credentials in plain text. This is fine locally, but a serious security problem once the code goes to GitHub or a server.
Environment variables move sensitive values outside the codebase entirely.
Why This Matters
-
SECRET_KEYexposed in a public repo is a security vulnerability -
DEBUG=Truein production leaks error details to users - Database credentials in code means anyone with repo access has database access
python-decouple
python-decouple is the cleanest way to handle this in Django:
pip install python-decouple
Setting Up .env
Create a .env file at the project root:
SECRET_KEY=your-actual-secret-key-here
DEBUG=True
DATABASE_URL=sqlite:///db.sqlite3
ALLOWED_HOSTS=localhost,127.0.0.1
Immediately add .env to .gitignore:
# .gitignore
.env
env/
__pycache__/
*.pyc
staticfiles/
media/
Never commit .env to version control.
Using decouple in settings.py
from decouple import config, Csv
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost', cast=Csv())
-
config('KEY')— reads the value from.env -
default— fallback if the key isn't found -
cast— converts the string value to the correct Python type -
Csv()— splits a comma-separated string into a list
Database URL with dj-database-url
For database configuration, dj-database-url parses a database URL string into Django's DATABASES format:
pip install dj-database-url
import dj_database_url
from decouple import config
DATABASES = {
'default': dj_database_url.config(
default=config('DATABASE_URL')
)
}
In .env:
DATABASE_URL=sqlite:///db.sqlite3
For PostgreSQL in production:
DATABASE_URL=postgres://user:password@localhost:5432/mydb
Switching databases becomes a one-line change in .env — no touching settings.py.
Separate Settings for Development and Production
A common pattern is having separate settings files:
mysite/
settings/
__init__.py
base.py # shared settings
development.py # dev-specific
production.py # production-specific
# base.py — shared across all environments
SECRET_KEY = config('SECRET_KEY')
INSTALLED_APPS = [...]
TEMPLATES = [...]
# development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
Run with a specific settings file:
python manage.py runserver --settings=mysite.settings.development
Or set it in the environment:
DJANGO_SETTINGS_MODULE=mysite.settings.development
A .env.example File
Since .env is gitignored, other developers cloning your repo won't know what variables are needed. Create a .env.example file with the keys but no values, and commit that:
# .env.example
SECRET_KEY=
DEBUG=
DATABASE_URL=
ALLOWED_HOSTS=
This is a convention every professional Django project follows.
Putting It All Together — Production Checklist
Before deploying any Django project, run through this:
python manage.py check --deploy
Django's built-in deployment check will flag any security issues. Common things it catches:
-
DEBUG = True— must beFalsein production -
SECRET_KEYtoo short or predictable - Missing
SECURE_SSL_REDIRECT - Missing
SESSION_COOKIE_SECURE -
ALLOWED_HOSTSempty or too permissive
Address every warning before going live.
Wrapping Up
Static files, media files, and environment variables are the unglamorous but essential parts of any Django project. Static files for design assets, media files for user uploads, and environment variables to keep secrets out of the codebase. Combined with the deployment checklist, the project is now actually ready to leave the local machine.
Thanks for reading. Feel free to share your thoughts!
Top comments (0)