
The easy default for a website is to let the application serve everything: HTML, routing, business behavior, CSS, JavaScript, images, fonts, downloads, and every generated asset that happens to land under wwwroot.
That default is convenient. It is also one of those engineering shortcuts that becomes harder to defend as the system matures.
Static assets do not have the same lifecycle as application logic. They need different caching rules, different deployment concerns, different invalidation behavior, and often a different operational blast radius. A Razor Pages app, MVC site, API host, or worker-backed web system should not have to be the origin for every public byte merely because that was the shortest path during project setup.
Static Content Provider is the Codebelt Tool for that boundary. Its source project, Codebelt.Cdn.Origin, is a small ASP.NET Core and Kestrel application that serves physical files from a configured content root. It can run as a separate asset host for a website, or as the custom origin behind a CDN provider such as AWS CloudFront, Cloudflare, Azure Front Door, or Google Cloud CDN.
The design goal is deliberately narrow: serve public static content safely, with a correct HTTP contract, without turning the origin into another business application.
The old reason was domain sharding
Dedicated asset hosts are not new.
In the HTTP/1.x era, teams often used multiple asset domains to work around browser connection limits. Splitting requests across static1.example.com, static2.example.com, and static3.example.com increased parallel downloads when one TCP connection could only carry limited concurrent work.
That reason has aged out. HTTP/2 was standardized specifically to make better use of a connection, including multiple concurrent exchanges on the same connection. Codebelt.Cdn.Origin says the same thing in its own README: on HTTP/2 and HTTP/3, the connection-count argument for separate asset domains is not the point anymore, and old-style domain sharding can be counter-productive.
The architectural reason still holds.
Static content is a different deployment concern. It can be served from a smaller runtime surface, cached more aggressively, mounted or baked read-only, and placed behind CDN semantics that are inappropriate for dynamic application responses. The goal is no longer "more browser connections." The goal is segregation of duties.
That difference matters.
A cleaner host model
A useful production topology separates three concerns:
www.example.com -> application and website rendering
assets.example.com -> site-owned static assets
cdn.example.com -> shared, versioned resources used across applications
The names are examples, not a naming rule. The important part is ownership.
www.example.com owns application behavior. It can deploy when routes, pages, services, authentication, data access, or business behavior change.
assets.example.com owns content that belongs to that particular site: stylesheets, scripts, images, icons, generated bundles, and article media. Those files can follow the website content lifecycle without forcing the application host to serve them.
cdn.example.com owns shared resources: versioned libraries, fonts, design-system artifacts, framework scripts, documentation assets, or anything that multiple applications consume. That surface should strongly prefer immutable, explicitly versioned URLs and long-lived cache headers.
This model is not more complex for its own sake. It is a way to keep the system honest about what each host is responsible for.
What the Static Content Provider adds
Codebelt.Cdn.Origin does not try to replace ASP.NET Core static-file behavior. It packages the right subset of it into a focused deployable tool.
In the current 2.0.0 line, the provider:
- serves files from
CdnOrigin:ContentRoot, defaulting to/cdnroot; - validates the content root at startup so missing, unreadable, or application-overlapping roots fail before traffic is served;
- uses ASP.NET Core static-file middleware for
GETandHEAD, content type handling, validators, conditional requests, range requests, and default documents; - rejects unknown file types by default unless explicit MIME mappings are configured;
- supports two cache profiles: revalidate for mutable URLs and immutable for versioned or content-addressed paths;
- supports configurable CORS, optional response compression, and health endpoints;
- maps unsupported methods against existing files to
405 Method Not Allowedwith anAllowheader; - runs as a non-root container on port
8080, with/cdnrootas the read-only content mount.
That is the right shape for an origin. It has enough behavior to be a reliable static host and intentionally little else.
It is not an upload service. It is not an object store. It is not a reverse proxy. It is not a dynamic website. It is not a place to add "just one small endpoint."
That restraint is the feature.
HTTP semantics are the contract
A static origin is only useful if caches, browsers, and CDN providers can trust its responses.
For mutable URLs, the default revalidate profile emits a public response with max-age=12h, s-maxage=7d, and must-revalidate. That lets browsers and shared caches retain the response while still giving the origin a clear validation path.
For versioned or content-addressed URLs, the immutable profile emits a public one-year freshness lifetime with immutable. That is the profile to use for paths whose content will never change after publication.
The important part is that these are explicit profiles, not a single generic header sprayed onto every file. A mutable article image and a versioned framework bundle are not the same kind of asset.
The provider also avoids several old habits:
- it no longer hashes file contents per request to build
ETagvalues; - it does not emit
no-transformby default, so a CDN can legitimately optimize responses; - it does not rely on
ExpireswhenCache-Control: max-ageis the authoritative freshness signal; - it does not cache responses inside the origin process when the browser and CDN are the actual cache layers;
- it does not serve arbitrary unknown file extensions by default.
These choices keep the origin small and predictable. They also lean on the framework where the framework already owns the hard HTTP behavior.
Microsoft's ASP.NET Core static-files guidance documents the same baseline: static files are served with headers such as ETag, Last-Modified, and Content-Type, and the framework's static-file support is the correct place to handle that behavior. The Codebelt provider adds deployment shape, validation, explicit cache profiles, and a hardened container boundary around that baseline.
Two roles, one origin
Static Content Provider has two practical roles.
The first role is segregated web assets. A site can continue authoring assets in the normal project structure, then publish those files into a separate image or mount. The application renders HTML and references the asset host. The asset host serves the files.
For local development or a small deployment, that may be enough:
Browser -> www.example.com -> ASP.NET Core application
-> assets.example.com -> web-cdn-origin -> /cdnroot
The second role is CDN origin. In that mode, a provider such as CloudFront or Cloudflare sits in front of the origin. The CDN caches at the edge, honors or overrides cache headers according to its configuration, and sends cache misses or revalidations back to Codebelt.Cdn.Origin.
Browser -> cdn.example.com -> CDN edge -> web-cdn-origin -> /cdnroot
The same origin can serve both strategies because the core requirement is the same: a small, read-only HTTP surface with correct cache and file-serving semantics.
A minimal runtime path
The container path is intentionally direct. The published image sets ASPNETCORE_HTTP_PORTS=8080 and CdnOrigin__ContentRoot=/cdnroot, exposes port 8080, and runs as user 65532.
A local run can be as small as:
docker run -d --name cdn-origin \
--read-only \
-p 8080:8080 \
-v /path/to/content:/cdnroot:ro \
codebeltnet/web-cdn-origin:2.0.0
For content-hashed or versioned paths, add an immutable prefix:
CdnOrigin__Cache__ImmutablePathPrefixes__0=/assets/
Then keep the rule strict: only publish files under that prefix when the URL is immutable. If a file may be replaced in place, keep it under the revalidate profile.
That discipline is what makes CDN behavior boring in the best possible way.
The skill that moves an ASP.NET Core app there
The tool provides the runtime origin. Migration is a separate practice.
That is where .NET Segregated Assets fits. The skill is designed for ASP.NET Core applications that should keep authoring static files in wwwroot while serving deployed app assets from codebeltnet/web-cdn-origin. It inspects the application, preserves application-owned versus shared CDN ownership, and wires the local and deployment topology without asking the website to abandon its normal authoring workflow.
The source for that skill is available in the Codebelt agentic repository.
That distinction is important. A tool should not pretend to be a migration plan. A skill should not pretend to be a runtime. The two belong together because they own different parts of the same architectural move.
Recommended usage
Use Static Content Provider when static delivery should be separated from the main web application and one of these statements is true:
- the application should stop shipping or serving its own public assets;
- site-owned assets need independent deployment or scaling;
- a CDN should sit in front of a small custom origin instead of the business application;
- cache policy needs to differ between mutable URLs and immutable versioned URLs;
- public file serving should be read-only, explicit, and boring;
- the team wants to avoid inventing static-file HTTP behavior that ASP.NET Core already provides correctly.
Do not use it to hide architectural uncertainty. If assets are still tightly coupled to dynamic rendering, credentials, per-user authorization, runtime generation, or private data access, keep that behavior in the application until the boundary is real.
Static segregation works when the content is genuinely public, file-shaped, and cacheable.
Better architecture is usually less convenient at first
The easiest solution is often to let the web app do everything. That is why so many systems keep assets, pages, APIs, and operational behavior in one deployment long after the initial convenience has stopped paying for itself.
Static content is a good place to push back.
Separating assets from application logic gives the system a clearer shape. It makes caching a first-class contract. It lets a CDN do CDN work. It gives operations a smaller origin to reason about. It also makes the application host less responsible for bytes that do not need application logic at all.
That is the practical value of web-cdn-origin: not a nostalgia pass for domain sharding, and not a custom static-file framework.
It is a small, deliberate runtime boundary for content that deserves its own lifecycle.
Timeline
The web-cdn-origin checkout used for this article has local-complete history, but its Git tags and changelog headings drift from the image versions that were actually published to Docker Hub after the early 1.x line. The timeline below therefore treats Docker Hub's stable image tags as the version ledger, then cross-checks local Git commits and changelog dates to explain what each published image contained.
v1.0.02021-04-28The first published image was a small ASP.NET Core static origin with cache validators and permissive CORS.
The initial source introduced
ProgramandStartup, served a configured content path from/cdnroot, emittedCache-Control,Expires,Last-Modified, and a content validator, and allowed cross-origin access for asset delivery.v1.1.02021-04-29The origin became container-first and learned default documents.
By the time Docker Hub published
1.1.0, configuration had moved to environment variables such asCDNROOT,CACHECONTROL_*, andCDNROOT_DEFAULTFILES, and the pipeline could resolveindex.htmlanddefault.htmbefore static-file serving.v1.1.52021-05-02The 1.1 line hardened cross-platform asset delivery.
Docker Hub published intermediate
1.1.1through1.1.4images while the provider switched to case-insensitive file lookup, added MD5-basedETaggeneration, and madeETAG_BYTESTOREADconfigurable. By1.1.5, CORS had moved ontoOnPrepareResponse, so the static-file response itself emittedAccess-Control-Allow-Originalongside the cache and validator headers.v1.2.02021-05-20Response compression joined the origin pipeline.
The provider added ASP.NET Core response compression and placed it before static-file serving, giving compressible assets a first implementation of Gzip/Brotli delivery while keeping the same file-origin model.
v1.2.12021-12-11The published image moved to .NET 6.
Docker Hub
1.2.1aligns with the same source snapshot that the repository later labeledv1.3.0. In source terms, this was the .NET 6 retarget rather than a redesign of the asset-serving pipeline.v1.3.02022-11-22The next published image moved to .NET 7.
Docker Hub
1.3.0aligns with thenet7.0retarget that the repository later labeledv1.4.0. The static-file contract stayed the same, but the runtime baseline advanced again.v1.4.02024-04-30The final 1.x image moved to .NET 8 and refreshed its Alpine container base.
Docker Hub
1.4.0aligns with the April 2024 ALM commit rather than the later unpublishedv1.4.1Git tag. The project retargetednet8.0and updated both the ASP.NET and SDK base images to 8.0.v2.0.02026-09-26(current)The origin became a modern .NET 10, strongly configured, CDN-ready runtime.
The
2.0.0release replaced the legacy startup model with modern minimal hosting, introduced theCdnOriginoptions section, added explicit revalidate and immutable cache profiles, made CORS, compression, content types, and health checks configurable, removed per-request content hashing forETag, and hardened the container around non-root port8080with/cdnrootas the content mount.
Sources
- Static Content Provider on Codebelt
- web-cdn-origin source repository
- web-cdn-origin Docker Hub tags
- web-cdn-origin README
- web-cdn-origin changelog
- Static files in ASP.NET Core — Microsoft Learn
- Response compression in ASP.NET Core — Microsoft Learn
- HTTP/2, RFC 9113
- Use various origins with CloudFront distributions — AWS
- Default cache behavior — Cloudflare
- .NET Segregated Assets on Codebelt
- .NET Segregated Assets source
