False Flag Operation

Oct. 4, 2025


A False Flag operation is an attack carried out to make it appear that some other actor conducted it. That term reminds me of how Django's DEBUG flag works on Heroku. It can be difficult to properly configure static and media file serving on Heroku in order to deploy your Django app on Heroku with DEBUG=False. Incorrect settings can lead to 500 errors when DEBUG=False, but those errors can often be masked by setting DEBUG=True. Don't be content masking the errors... conduct a False Flag operation until your app is properly configured to run with DEBUG=False on Heroku.

I have been working with Heroku and Django for over nine years. However, when I deployed my first Django app to Heroku, I dealt with numerous issues setting up static and media file sharing from Heroku. The first hint of trouble when I deployed my Django app to Heroku was ValueError: Missing staticfiles manifest entry for 'css/bootstrap.min.css' But when I set DEBUG=True everything worked again. Or so it seemed. I was confused why my app would serve static files fine with DEBUG set to True, but would immediately fail when the DEBUG environment variable was set to False. I eventually relized that Django was running a false flag operation. In debug mode, Django uses finders defined in the Django settings file(s) to scan the project tree for static files at runtime. Everything “looked fine” — but in reality, the staticfiles manifest was missing, my build was broken, and I had a ticking time bomb in production.

The rest of this post shows sample settings for configuring Django to serve static files using either WhiteNoise or AWS based on the setting of two environment variables: USE_S3_STATIC and USE_S3_MEDIA.


Understanding how Heroku uses DEBUG to determine how to serve static files

  • With DEBUG=False, Django insists on using a manifest (staticfiles.json) to serve hashed assets. No manifest? Hard stop.
  • With DEBUG=True, Django waves a false flag and says: “Don’t worry, I’ll just search around myself.” It calls in the finders, pulls assets directly from the app slug, and pretends everything’s fine.

The Fix: Locking Down Production Settings

The first step in shutting down the false flag operation is to make your production storage explicit and bulletproof. Below are sample settings that have been proved to work for static file serving using WhiteNoise or AWS depending on how the USE_S3_STATIC and USE_S3_MEDIA environment variables are set.

"""
settings/production.py
Self-contained example for Django + Heroku + S3/WhiteNoise
"""

import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
import environ

env = environ.Env()
BASE_DIR = Path(__file__).resolve().parent.parent

DEBUG = env.bool("DEBUG", default=False)

# Defaults (local disk) — overridden below if S3 media is enabled
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# ── Kill legacy storage knobs to avoid Django 4+ conflicts ─────────────
globals().pop("DEFAULT_FILE_STORAGE", None)
globals().pop("STATICFILES_STORAGE", None)

# ── Feature flags (env toggles) ────────────────────────────────────────
USE_S3_STATIC = env.bool("USE_S3_STATIC", default=True)   # S3 vs WhiteNoise
USE_S3_MEDIA  = env.bool("USE_S3_MEDIA",  default=True)   # Media should be S3 in prod

STORAGES = {}

# ── Shared S3 config if either path uses S3 ────────────────────────────
if USE_S3_STATIC or USE_S3_MEDIA:
    AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
    if not AWS_STORAGE_BUCKET_NAME:
        raise ImproperlyConfigured("Set AWS_STORAGE_BUCKET_NAME")
    AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default="us-west-2")
    AWS_S3_CUSTOM_DOMAIN = env(
        "AWS_S3_CUSTOM_DOMAIN",
        default=f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com",
    )
    AWS_QUERYSTRING_AUTH = False
    AWS_S3_FILE_OVERWRITE = False
    AWS_DEFAULT_ACL = None  # django-storages best practice

# ── Media storage (S3 recommended on Heroku) ───────────────────────────
if USE_S3_MEDIA:
    STORAGES["default"] = {
        "BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
        "OPTIONS": {"location": "media"},
    }
else:
    MEDIA_URL = "/media/"
    MEDIA_ROOT = BASE_DIR / "media"
    STORAGES["default"] = {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
        "OPTIONS": {"location": str(MEDIA_ROOT)},
    }

# ── Static storage: S3 or WhiteNoise ───────────────────────────────────
if USE_S3_STATIC:
    STORAGES["staticfiles"] = {
        "BACKEND": "storages.backends.s3boto3.S3ManifestStaticStorage"
    }
    STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"

    # WhiteNoise not needed on S3 — remove to silence “No directory at /app/staticfiles/”
    try:
        MIDDLEWARE.remove("whitenoise.middleware.WhiteNoiseMiddleware")
    except ValueError:
        pass
else:
    STORAGES["staticfiles"] = {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"
    }
    STATIC_URL = "/static/"
    STATIC_ROOT = BASE_DIR / "staticfiles"
    # WhiteNoise middleware stays active here

Keep in mind it is important to ensure collectstatic is properly run in order to build and deploy static assets including the manifest file. Follow the steps below to ensure collectstatic is properly run remembering to replace <app> with the name of your Heroku app (note: this blog assumes users are using the Heroku CLI from a MacOS terminal):

heroku config:unset DISABLE_COLLECTSTATIC -a <app>

# This confirms the plan works, but those files vanish when the dyno self-destructs...beware of Heroku's temporary dynos!
heroku run -a <app> bash
python manage.py collectstatic --noinput --settings=settings.production

# Trigger collectstatic during the real build
# During slug build, collectstatic runs for real, and /app/staticfiles/staticfiles.json is baked into the slug.
git commit --allow-empty -m "Trigger collectstatic"
# heroku below is the alias to your Heroku git repository
git push heroku HEAD:master 

Interrogate the runtime (you can run the Heroku CLI from a terminal window):

heroku run -a <app> -- python - <<'PY'
from django.conf import settings
print("STATIC_URL:", settings.STATIC_URL)
print("static backend:", settings.STORAGES['staticfiles'])
from django.core.files.storage import default_storage
print("media backend:", default_storage.__class__.__module__)
print("WhiteNoise active:", any('WhiteNoiseMiddleware' in m for m in settings.MIDDLEWARE))
PY

Verify the target asset is in the manifest

heroku run -a <app> -- python manage.py findstatic css/bootstrap.min.css -v 2

S3 vs WhiteNoise: Choosing Your static/media serving option:

Why S3 is the logical choice most of the time

  • Persistence: Dyno restarts don’t wipe your files
  • Scalability: Offload serving to S3/CloudFront
  • Performance: Hashed, cacheable URLs
  • Best practice: Media must go here; static should too once you scale

Why WhiteNoise Has Its Place

  • MVPs / small apps: simplest setup, no buckets, no AWS IAM policies to configure
  • Static-only apps: no media means slug-only is fine
  • Staging/dev: keep parity without extra infrastructure
  • Minimal setup: no S3 operational overhead

The Hybrid Option

  • Media → S3 always
  • Static → configurable (USE_S3_STATIC toggle)
    • WhiteNoise for simplicity
    • S3 for scale
  • Flip an env var, Heroku automatic redeployment, done.

Why DEBUG=True Is a False Flag

When I flipped DEBUG=True, everything “worked” again — but for the wrong reasons: Django skipped the manifest check and used finders to locate static files directly. That masked the missing manifest problem. Worse, DEBUG=True disables ALLOWED_HOSTS checks and exposes full error pages with secrets, environment variables, and stack traces. It’s like putting duct tape on your smoke alarm — you silence the noise, but the house still burns down.

How Django Resolves Static Files during Build (Heroku slug compilation):

collectstatic
   │
   ├─> FileSystemFinder
   ├─> AppDirectoriesFinder
   └─> CompressorFinder
   result:
      - WhiteNoise → /app/staticfiles + manifest
      - S3 → uploads to bucket + manifest

Runtime With DEBUG=True

{% static "css/bootstrap.min.css" %}
        │
        └─> Bypass manifest
             └─> Finders scan repo, serve raw file

✅ Appears to work. ❌ Actually a false flag: no manifest, no cache busting, insecure, ignores ALLOWED_HOSTS, leaks stack traces.

Runtime With DEBUG=False

{% static "css/bootstrap.min.css" %}
        │
        └─> Strict manifest lookup
              ├─ Found → /static/css/bootstrap.<hash>.css (200)
              └─ Missing → ValueError → 500

Strict, correct, cacheable.

tl/dr

Takeaways

🟥 Never run with DEBUG=True in production. It’s a false flag

🟩 Always run collectstatic during slug build, not in a one-off dyno

🟩 Use S3 for media, always. Static can be WhiteNoise or S3

🟩 Hybrid toggle (USE_S3_STATIC, USE_S3_MEDIA) gives flexibility

🟩 Verify with findstatic and sanity checks

If your Django app fails to run properly on Heroku with DEBUG=False but works with DEBUG=True then that is a strong indicator that there are configuration issues with how media and static file serving is configured within the application. This blog has pointed out why allowing your app to run with DEBUG=True is not advisable, and it contains sample settings and effective ways to validate file serving is configured correctly.

Comment Enter a new comment: