DEV Community

Ajit Kumar
Ajit Kumar

Posted on

Django Static Files: The Complete Guide - From Local Dev to S3 Production with Tailwind CSS

Django Static Files: The Complete Guide

Managing static files in Django becomes complex when you add Tailwind CSS compilation, AWS S3 storage, Docker containerization, and CI/CD pipelines. This guide explains how all these pieces fit together.


Table of Contents


Understanding the Basics

What Are Static Files?

Static files in Django are files that don't change per request:

  • CSS stylesheets (including compiled Tailwind CSS)
  • JavaScript files
  • Images, fonts, icons
  • Third-party libraries (Bootstrap, jQuery, etc.)

What Are Media Files?

Media files are user-uploaded content:

  • Profile pictures
  • Documents, PDFs
  • User-generated images/videos

Why This Gets Complex

The complexity comes from multiple interconnected systems:

The Static Files Pipeline:

Developer → Tailwind → Django → Storage → CDN → Browser
   ↓         ↓          ↓         ↓        ↓       ↓
 Write    Compile   Collect   Upload   Cache   Load
  CSS      CSS      Static     S3              Files
Enter fullscreen mode Exit fullscreen mode

Each step requires specific configuration:

  1. Multiple environments: Local, staging, production
  2. Build steps: CSS compilation (Tailwind, Sass)
  3. Storage backends: Local filesystem vs. S3/CDN
  4. Deployment: Docker, CI/CD pipelines
  5. Performance: Caching, compression, CDN

The Problem Space

Common Scenario

You have a Django project that:

  • ✅ Uses Tailwind CSS (requires build step)
  • ✅ Serves static files from AWS S3 in production
  • ✅ Uses local storage in development
  • ✅ Deploys via GitHub Actions + Docker
  • ✅ Has separate dev/staging/prod environments

The Question Everyone Asks

"Why doesn't my compiled Tailwind CSS show up in production after deployment?"

This reveals 5 interconnected systems that must work together:

The Integration Chain:

npm build → Django settings → collectstatic → Storage backend → Template tags → Browser
    ↓            ↓                 ↓                ↓                ↓           ↓
 output.css   Finds file       Copies file      Uploads S3       Gets URL    Loads CSS
Enter fullscreen mode Exit fullscreen mode

If any link breaks, CSS won't load.


Django Static Files Architecture

Two Distinct Phases

Phase 1: Development (DEBUG=True)

How it works:

Browser Request → Django Dev Server → Searches STATICFILES_DIRS → Returns File
Enter fullscreen mode Exit fullscreen mode

Django's development server (runserver) serves static files automatically.

Phase 2: Production (DEBUG=False)

How it works:

Developer runs collectstatic → Django uploads to S3
                                      ↓
Browser Request → Nginx → S3/CloudFront → Returns File
Enter fullscreen mode Exit fullscreen mode

Django does NOT serve static files in production. You need a web server or CDN.

Key Django Settings Explained

# 1️⃣ Where Django FINDS static files (sources)
STATICFILES_DIRS = [BASE_DIR / 'static']

# 2️⃣ Where Django COLLECTS static files (destination)
STATIC_ROOT = BASE_DIR / 'staticfiles'

# 3️⃣ URL prefix for static files
STATIC_URL = '/static/'

# 4️⃣ How Django STORES collected files
STORAGES = {
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
    }
}
Enter fullscreen mode Exit fullscreen mode

The Mental Model

Think of it like organizing books:

Django Setting Book Analogy Purpose
STATICFILES_DIRS Your bookshelves at home Where Django looks for files
STATIC_ROOT The library collection room Temporary staging area
STORAGES The filing system How files are organized/stored
STATIC_URL The library's address How users access files

Complete Flow Diagram

Development vs Production:

┌─────────────────────────────────────────────────────┐
│              DEVELOPMENT (DEBUG=True)                │
├─────────────────────────────────────────────────────┤
│                                                      │
│  Your static files (static/css/output.css)          │
│         ↓                                            │
│  STATICFILES_DIRS points to static/                  │
│         ↓                                            │
│  Django dev server finds and serves directly         │
│         ↓                                            │
│  Browser loads from /static/css/output.css          │
│                                                      │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│             PRODUCTION (DEBUG=False)                 │
├─────────────────────────────────────────────────────┤
│                                                      │
│  Your static files (static/css/output.css)          │
│         ↓                                            │
│  STATICFILES_DIRS points to static/                  │
│         ↓                                            │
│  collectstatic command runs                          │
│         ↓                                            │
│  Copies to STATIC_ROOT (staticfiles/)               │
│         ↓                                            │
│  Storage Backend (S3Boto3Storage)                   │
│         ↓                                            │
│  Uploads to S3 bucket                               │
│         ↓                                            │
│  Browser loads from S3 URL                          │
│                                                      │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

How It All Fits Together

The Complete Pipeline

When you use Tailwind CSS with S3 storage and Docker deployment:

Local Development Flow:

Write HTML with Tailwind → npm run dev --watch → output.css generated
                                                          ↓
                                                  Django runserver
                                                          ↓
                                              Browser loads from /static/
Enter fullscreen mode Exit fullscreen mode

CI/CD & Production Flow:

git push → GitHub Actions Triggered
              ↓
         npm run build (creates output.css)
              ↓
         collectstatic (finds file via STATICFILES_DIRS)
              ↓
         Upload to S3 (via Storage Backend)
              ↓
         Build Docker image (app code only)
              ↓
         Deploy container
              ↓
         Browser requests CSS → Nginx → S3/CloudFront → Cached CSS
Enter fullscreen mode Exit fullscreen mode

Critical Understanding Points

1. Tailwind CSS Must Be Built BEFORE collectstatic

npm run build → output.css → collectstatic finds it → uploads to S3
Enter fullscreen mode Exit fullscreen mode

If you run collectstatic before building CSS, Django won't find the file.

2. STATICFILES_DIRS Must Be Set for BOTH Local and S3

This is the #1 most common mistake:

# ❌ WRONG - Only set for local
if USE_S3_STORAGE:
    STORAGES = {...}
    # Missing STATICFILES_DIRS!
else:
    STATICFILES_DIRS = [BASE_DIR / 'static']

# ✅ CORRECT - Set for both
if USE_S3_STORAGE:
    STORAGES = {...}
    STATICFILES_DIRS = [BASE_DIR / 'static']  # ← Must be here!
else:
    STATICFILES_DIRS = [BASE_DIR / 'static']
Enter fullscreen mode Exit fullscreen mode

3. Templates Must Use Django Static Tag

<!-- ✅ CORRECT - Adapts to environment -->
{% raw %}{% load static %}{% endraw %}
<link rel="stylesheet" href="{% raw %}{% static 'css/output.css' %}{% endraw %}">

<!-- ❌ WRONG - Hardcoded, won't use S3 -->
<link rel="stylesheet" href="/static/css/output.css">
Enter fullscreen mode Exit fullscreen mode

4. Docker Image Doesn't Need Static Files

Since files are on S3, your Docker image only needs application code:

Docker Image: Python + App Code (small, fast)
Static Files: On S3 (served separately)
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls & Solutions

Problem 1: CSS Not Updating After Deployment

Symptoms: Old styles load, changes work locally but not in production

Root Causes:

  1. Browser/CDN cache
  2. collectstatic didn't run
  3. Files uploaded to wrong S3 bucket

Diagnosis:

# Check if CSS was built
ls -lh static/css/output.css

# Check if uploaded to S3
aws s3 ls s3://your-bucket/static/css/

# Check browser (DevTools → Network tab)
# Look for output.css and verify the URL
Enter fullscreen mode Exit fullscreen mode

Solutions:

  • Clear browser cache (Ctrl+Shift+R)
  • Invalidate CloudFront cache
  • Add cache busting with ManifestStaticFilesStorage
  • Verify GitHub Actions workflow completed successfully

Problem 2: collectstatic Finds 0 Files

Symptoms:

0 static files copied to 'staticfiles'
Enter fullscreen mode Exit fullscreen mode

Diagnosis Flow:

collectstatic finds 0 files
         ↓
    Is STATICFILES_DIRS set?
         ↓
    NO → ❌ Add STATICFILES_DIRS to settings
         ↓
    YES → Does file exist?
         ↓
    NO → ❌ Run npm run build first
         ↓
    YES → Is path correct?
         ↓
    NO → ❌ Fix path in STATICFILES_DIRS
         ↓
    YES → Is USE_S3_STORAGE set correctly?
         ↓
    NO → ❌ Check environment variable
Enter fullscreen mode Exit fullscreen mode

Quick Test:

# Should find your CSS file
python manage.py findstatic css/output.css
Enter fullscreen mode Exit fullscreen mode

Problem 3: CSS Works Locally, Fails in CI/CD

Common Causes:

  • CSS file in .gitignore but not built in CI
  • Wrong working directory in GitHub Actions
  • Missing npm ci before npm run build
  • Environment variables not passed to step

Solution Checklist:

  • ✅ Ensure npm run build runs in CI
  • ✅ Verify file created: ls -lh static/css/output.css
  • ✅ Pass AWS credentials to collectstatic step
  • ✅ Set USE_S3_STORAGE=True in environment

Problem 4: S3 Permissions Error

Symptoms:

botocore.exceptions.ClientError: An error occurred (AccessDenied)
Enter fullscreen mode Exit fullscreen mode

Required IAM Permissions:

  • s3:PutObject
  • s3:PutObjectAcl
  • s3:GetObject
  • s3:ListBucket

Quick Test:

# Test credentials
aws s3 ls s3://your-bucket
Enter fullscreen mode Exit fullscreen mode

Problem 5: CORS Errors

Symptoms:

Access to CSS from origin 'https://yourdomain.com' has been blocked by CORS
Enter fullscreen mode Exit fullscreen mode

Solution: Configure S3 bucket CORS policy to allow your domain.


Configuration Examples

All complete configuration examples are available in this GitHub Gist:

📦 Complete Django Static Files Configuration

The gist includes:

1. Django Settings

  • config/settings/base.py - Complete S3 configuration
  • config/settings/dev.py - Local development
  • config/settings/prod.py - Production settings
  • apps/core/storage_backends.py - S3 storage classes

2. Tailwind Configuration

  • package.json - NPM scripts
  • tailwind.config.js - Tailwind setup
  • static/css/input.css - Tailwind source

3. Docker Configuration

  • Dockerfile.web - Optimized production Dockerfile
  • docker-compose.yml - Local development setup

4. CI/CD Pipeline

  • .github/workflows/deploy.yml - Complete GitHub Actions workflow
  • ✅ Environment variable configuration
  • ✅ Multi-environment deployment (dev/prod)

5. Templates

  • templates/base.html - Proper static file usage
  • ✅ Example component templates

6. Additional Files

  • .gitignore - Recommended exclusions
  • .env.example - Environment variables template
  • requirements.txt - Python dependencies

🔗 View the complete configurations on GitHub Gist →


Quick Start Guide

Prerequisites

# Install required packages
pip install django-storages[s3] boto3 django-environ
npm install -D tailwindcss
Enter fullscreen mode Exit fullscreen mode

Step 1: Configure Django Settings

Copy the settings from the GitHub Gist and adjust:

  • AWS credentials
  • Bucket name
  • Region

Step 2: Create Storage Backends

Create apps/core/storage_backends.py using the template in the gist.

Step 3: Setup Tailwind

Initialize Tailwind and create necessary files (see gist for complete setup).

Step 4: Configure CI/CD

Copy the GitHub Actions workflow and add required secrets:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_STORAGE_BUCKET_NAME
  • AWS_REGION_NAME

Step 5: Test Locally

# Terminal 1: Watch CSS changes
npm run dev

# Terminal 2: Run Django
python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

Step 6: Deploy

git add .
git commit -m "Configure static files with S3"
git push origin main
Enter fullscreen mode Exit fullscreen mode

GitHub Actions will automatically:

  1. Build Tailwind CSS
  2. Upload to S3
  3. Build Docker image
  4. Deploy

Directory Structure

your_project/
├── config/settings/      # Django settings
├── apps/core/           # Storage backends
├── static/              # Source static files
│   └── css/
│       ├── input.css    # Tailwind source
│       └── output.css   # Generated (git: optional)
├── staticfiles/         # Temp (gitignored)
├── templates/           # HTML templates
├── package.json         # NPM config
├── Dockerfile.web       # Docker config
└── .github/workflows/   # CI/CD
Enter fullscreen mode Exit fullscreen mode

Debugging Commands

# Verify Django can find your CSS
python manage.py findstatic css/output.css

# Test collectstatic (dry-run)
python manage.py collectstatic --dry-run

# Check what files Django will collect
python manage.py collectstatic --dry-run | grep css

# List S3 bucket contents
aws s3 ls s3://your-bucket/static/css/

# Download from S3 to verify
aws s3 cp s3://your-bucket/static/css/output.css ./verify.css
Enter fullscreen mode Exit fullscreen mode

The Golden Rules

  1. Every static file must be in STATICFILES_DIRS for Django to find it
  2. Always use {% static %} tag in templates
  3. Build CSS before collectstatic in CI/CD
  4. Set STATICFILES_DIRS for both local AND S3 configurations
  5. Pass environment variables to collectstatic in CI/CD

Mental Model Summary

The Complete Static Files Pipeline:

┌──────────────┐
│ Write HTML   │
│ with Tailwind│
└──────┬───────┘
       ↓
┌──────────────┐
│  npm build   │  ← Compiles Tailwind
└──────┬───────┘
       ↓
┌──────────────┐
│  output.css  │  ← Generated in static/css/
│  created     │
└──────┬───────┘
       ↓
┌──────────────────┐
│ STATICFILES_DIRS │  ← Django knows where to look
│ includes static/ │
└──────┬───────────┘
       ↓
┌──────────────┐
│ collectstatic│  ← Finds and collects files
│ runs         │
└──────┬───────┘
       ↓
┌──────────────┐
│ Storage      │  ← Uploads to S3
│ backend      │
└──────┬───────┘
       ↓
┌──────────────┐
│ Template tag │  ← Generates correct URL
│ in HTML      │
└──────┬───────┘
       ↓
┌──────────────┐
│ Browser      │  ← Loads from S3
│ loads CSS    │
└──────────────┘
Enter fullscreen mode Exit fullscreen mode

Remember: It's a pipeline. Every stage must be configured correctly for files to flow from development to production.


Common Mistakes Checklist

❌ Mistake ✅ Correct
STATICFILES_DIRS only in local config Add to S3 config too
Hardcoded /static/ in templates Use Django static template tag
Building CSS in Docker Build in CI before Docker
Missing USE_S3_STORAGE=True in CI Set in environment variables
Forgetting npm ci before build Always install dependencies
Not passing AWS credentials to CI Add as environment variables

Further Optimization

CloudFront CDN

For faster global delivery, add CloudFront in front of S3 (configuration in gist).

Compression

Enable gzip for smaller file sizes (example in gist).

Cache Busting

Use ManifestStaticFilesStorage for automatic versioning (example in gist).


Resources


Conclusion

Managing static files in Django with Tailwind CSS and S3 requires understanding how multiple systems interact. The most common issue is forgetting to set STATICFILES_DIRS when USE_S3_STORAGE=True.

Key Takeaway: Django's static file system is a pipeline. Configure each stage correctly, and files will flow smoothly from development to production.

Got questions? Drop a comment below! 👇


Tags: #django #python #aws #s3 #tailwindcss #devops #cicd #docker #webdev

Top comments (0)