Serve Django Static and Media Files Through Bunny CDN

Put Bunny CDN in front of a Django site: create a pull zone for WhiteNoise static files or an object storage origin, use hashed file names with long cache headers, serve media from Bunny Storage with django-storages, and purge or sign URLs when needed.

Problem statement

Your Django site's CSS, JavaScript, and images load slowly for visitors far from your server, and serving them from Gunicorn workers or a single Nginx uses capacity you need for dynamic pages. Bunny CDN is a low-cost CDN with simple pull zones and its own edge storage. You can put it in front of static files without changing how Django collects them, and optionally store media in Bunny Storage.

Django with Bunny CDNcollectstatichashed namesOriginWhiteNoise or bucketPull zonecdn.example.comBrowserscached at edgeThe CDN fetches each hashed file once from Django and serves it from the edge afterwards.
  1. Use CompressedManifestStaticFilesStorage (WhiteNoise) so static files have hashed names and long cache headers.
  2. Create a Bunny pull zone with your site as the origin (https://shop.example.com) and a hostname such as cdn.example.com.
  3. Set STATIC_URL = "https://cdn.example.com/static/" in production.
  4. For media, use Bunny Storage with a storage zone and a django-storages backend, or keep media on S3/R2 and point a pull zone at it.
  5. Add CORS headers for fonts, and use purges or token authentication only where needed.

Step-by-step solution

1. Make static files cache-safe

STATIC_URL = os.environ.get("STATIC_URL", "/static/")
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
WHITENOISE_MAX_AGE = 31536000

Hashed file names (app.3f2a1c9b.css) change whenever content changes, so the CDN can cache them for a year without ever serving an outdated file. WhiteNoise sends Cache-Control: max-age=31536000, public, immutable for hashed files.

2. Create the pull zone

In the Bunny dashboard, create a pull zone with origin URL https://shop.example.com. Add a custom hostname cdn.example.com, create a CNAME record pointing to the pull zone's hostname, and enable the free TLS certificate. Leave Cache expiration time to respect origin headers, so WhiteNoise's long max-age applies to hashed files.

first and later requests1Page links cdn.example.comSTATIC_URL2Edge cache missfirst visitor nearby3Pull from Django originWhiteNoise serves4Edge stores for a yearimmutable header5Later hits from edgeorigin untouchedEach hashed file is fetched from Django roughly once per edge locationdjango-deployment.com
A new deploy creates new file names, so no purge is needed.

3. Point Django at the CDN

STATIC_URL=https://cdn.example.com/static/

Templates using {% static %} now produce CDN URLs. Fonts loaded from another origin need CORS: WhiteNoise adds Access-Control-Allow-Origin: * for static files by default (WHITENOISE_ALLOW_ALL_ORIGINS), and Bunny passes it through. Add cdn.example.com to your Content Security Policy for scripts, styles, and fonts.

4. Media options

  • Media in S3, R2, or another bucket: create a second pull zone with the bucket's public URL as origin, and set custom_domain in django-storages to media.example.com.
  • Media in Bunny Storage: create a storage zone and use a community django-storages backend for Bunny, or upload through Bunny's HTTP API. Attach a pull zone to the storage zone for delivery.

Private media should not go through a public pull zone; use Bunny's token authentication to sign URLs, or keep private files on presigned S3 URLs.

5. Purge only what is not hashed

Purging is rarely needed for hashed static files. For non-hashed files (a robots.txt or images uploaded with fixed names), purge by URL from the dashboard or API after changing them, or include a version in the file name.

file type and cachingEdge cacheInvalidationHashed static files1 year, immutablenew name per deployUnhashed staticshort, e.g. 1 hourpurge or renamePublic medialongunique upload namesPrivate media!no public cachesigned URLsUnique file names make long caching safedjango-deployment.com
django-storages with file_overwrite off gives each upload a unique name.

Explanation

A pull zone is a reverse proxy at Bunny's edge locations: on a cache miss it requests the file from your origin, stores it according to cache headers, and serves later requests directly. Django's manifest storage makes this safe by embedding a content hash in each static file's name. WhiteNoise provides correct headers from the origin, so the CDN configuration can stay minimal.

Edge cases / notes

  • Admin static files. Admin CSS and JS are collected and hashed like other static files and work through the CDN.
  • Origin shielding. Enabling Bunny's origin shield reduces requests to your server from many edge locations.
  • Compression. WhiteNoise serves pre-compressed Brotli and gzip variants; make sure the CDN forwards Accept-Encoding and varies on it.

Next steps

FAQ

Do I need to change collectstatic for Bunny CDN?

No. Keep collecting to STATIC_ROOT with WhiteNoise; only STATIC_URL changes.

Do I need to purge the CDN after every deploy?

Not for hashed static files. New content gets new file names.

Can Bunny serve private media?

Yes, with token-authenticated URLs, but private files often stay simpler with presigned object storage URLs.

Why do fonts fail to load from the CDN?

Missing CORS headers. WhiteNoise can add them, and the CSP must allow the CDN host.

Keep going