Configuring a CDN for CSS/JS/image deliveryfor Drupal 8 , 9 , 10 , and 11

Last updated :  

A CDN is like opening branch stores near your customers instead of shipping every order from one warehouse. Your Drupal server stays the single source of truth, but a network of edge servers scattered around the world hold cached copies of your CSS, JS, and images — so a visitor in Tokyo downloads those assets from a server in Tokyo, not from wherever your Drupal site actually lives.

What you'll learn in this lesson

  • What a CDN actually does, and why it matters most for static assets
  • The two real integration approaches: origin-pull CDNs and Drupal's own CDN module
  • How to point your public files path at a CDN in settings.php
  • The cache-invalidation gotcha that catches almost everyone the first time

Why static assets specifically

A CDN is a poor fit for dynamic, per-user HTML — that content genuinely has to come from your application server. But CSS, JS, and images are usually identical for every visitor and rarely change, which makes them ideal candidates: cache them once at every edge location, and every subsequent visitor near that edge gets a near-instant response instead of a round trip to your origin server.

Approach 1: origin-pull, no module needed

Most modern CDNs (Cloudflare, CloudFront, Fastly) work as a transparent reverse proxy in front of your whole site — you point your domain's DNS at the CDN, and it caches whatever your origin server returns, using the response's own Cache-Control headers to decide how long to keep it. This requires zero Drupal-side configuration beyond making sure your static assets are served with sensible cache headers (Drupal's own .htaccess/nginx config already sets long expiry on /sites/default/files/ and aggregated CSS/JS by default).

Approach 2: explicitly rewriting asset URLs

If you'd rather serve assets from a dedicated asset subdomain (cdn.example.com) instead of proxying your whole site, you rewrite the base URL Drupal uses when generating asset paths:

// In settings.php
$settings['file_public_base_url'] = 'https://cdn.example.com/sites/default/files';

This changes where Drupal points uploaded files (images, PDFs) — for CSS/JS aggregation specifically, the contributed CDN module is the standard tool: it lets you map file extensions or specific paths to CDN URLs through the admin UI (/admin/config/development/cdn) without touching settings.php at all, and it correctly handles fallback if the CDN is unreachable.

The gotcha that catches almost everyone: when you deploy new CSS/JS, Drupal generates a new aggregated file with a new hash in its filename — so cache invalidation happens automatically for aggregated assets. But if you're rewriting URLs for individual, non-aggregated files (a background image referenced directly in a stylesheet, say), the CDN may keep serving a stale cached copy under the old URL long after you've updated the file, since nothing in that URL changed to signal "this is new." Solve it by ensuring far-future-cacheable assets always have a content hash or version query string in their path.

A quick way to verify it's actually working

Open your browser's Network tab, reload a page, and check the "Initiator" and response headers on a CSS/JS/image request. A CDN response typically includes a header like cf-cache-status: HIT (Cloudflare) or x-cache: Hit from cloudfront — if you only ever see MISS, either the asset isn't cacheable (check its Cache-Control header) or the CDN isn't actually in the request path yet.

Quick check: you update a background image referenced directly in a custom CSS file (not run through Drupal's own aggregation), but visitors keep seeing the old version for hours after deploy. What's the most likely cause? If you said the CDN is still serving a cached copy under the unchanged filename, you've got it — without a version/hash in the path, nothing tells the CDN the underlying file changed.

Key takeaways

  • CDNs are for static, visitor-independent assets — CSS, JS, images — not for dynamic per-user page HTML.
  • Origin-pull CDNs (Cloudflare, CloudFront) need no Drupal-side config beyond correct Cache-Control headers, which Drupal already sets sensibly by default.
  • The contributed CDN module is the standard way to explicitly route specific asset types to a CDN subdomain through the admin UI.
  • $settings['file_public_base_url'] in settings.php redirects uploaded-file URLs specifically, not aggregated CSS/JS.
  • Always include a content hash or version string in the path of anything you want a CDN to invalidate automatically on change.

Coming up next

You've now covered library efficiency, conditional attaching, dependency management, compression, and CDN delivery — the complete asset-management toolkit. Next, we turn to a different rendering layer entirely: how Views, one of Drupal's most powerful but most performance-sensitive systems, can be tuned to stay fast as your dataset grows.