DEV Community

Cover image for Day 72 of 100 Days Of Code — Static Files, Media Files, and Environment Variables in Django
M Saad Ahmad
M Saad Ahmad

Posted on

Day 72 of 100 Days Of Code — Static Files, Media Files, and Environment Variables in Django

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'
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

You can also keep static files inside each app:

core/
    static/
        core/
            css/
                style.css
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

{% 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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

upload_to='posts/' means uploaded images go into media/posts/. Run migrations after adding this field.

ImageField requires the Pillow package:

pip install Pillow
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Save</button>
</form>
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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_KEY exposed in a public repo is a security vulnerability
  • DEBUG=True in 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Immediately add .env to .gitignore:

# .gitignore
.env
env/
__pycache__/
*.pyc
staticfiles/
media/
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
import dj_database_url
from decouple import config

DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL')
    )
}
Enter fullscreen mode Exit fullscreen mode

In .env:

DATABASE_URL=sqlite:///db.sqlite3
Enter fullscreen mode Exit fullscreen mode

For PostgreSQL in production:

DATABASE_URL=postgres://user:password@localhost:5432/mydb
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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())
Enter fullscreen mode Exit fullscreen mode

Run with a specific settings file:

python manage.py runserver --settings=mysite.settings.development
Enter fullscreen mode Exit fullscreen mode

Or set it in the environment:

DJANGO_SETTINGS_MODULE=mysite.settings.development
Enter fullscreen mode Exit fullscreen mode

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=
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Django's built-in deployment check will flag any security issues. Common things it catches:

  • DEBUG = True — must be False in production
  • SECRET_KEY too short or predictable
  • Missing SECURE_SSL_REDIRECT
  • Missing SESSION_COOKIE_SECURE
  • ALLOWED_HOSTS empty 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)