Production Builds, Granian Native Static & CDN Deployment¶
Moving a Litestar Vite application from development to production swaps the live Vite dev proxy for pre-compiled, content-hashed bundles (manifest.json), optional Rust-level static serving with Granian (StaticPlacement.NATIVE), and one-command cloud storage uploads (litestar assets deploy).
Building Production Bundles¶
Run litestar assets build during your container or CI build stage:
$ litestar assets build
✓ Exported .litestar.json, openapi.json, and routes.json
✓ Running vite build...
public/.vite/manifest.json 0.42 kB
public/assets/main-D4f1A9.css 12.84 kB │ gzip: 3.12 kB
public/assets/main-B9x8K2.js 148.20 kB │ gzip: 46.50 kB
✓ Built in 1.42s
This command:
- Exports
.litestar.json,openapi.json,routes.json, andinertia-pages.jsonso TypeScript types are up to date before bundling. - Runs
vite build(andvite build --ssrwhen SSR is enabled) using your configuredRuntimeConfig.executor(node,bun,deno,pnpm,yarn). - Emits hashed assets (
assets/main-B9x8K2.js,assets/main-D4f1A9.css) and<bundle_dir>/.vite/manifest.json(ormanifest.json).
Production Configuration & Granian Static Serving¶
In production (dev_mode=False), ViteAssetLoader reads manifest.json once at startup and resolves asset tags with O(1) dictionary lookups.
When paired with litestar-granian (GranianPlugin(static="auto")), VitePlugin exposes a StaticServerConfig contract (StaticPlacement.NATIVE) so Granian's Rust HTTP worker serves hashed bundle files directly before the request ever enters Python or ASGI:
"""Production asset serving, Granian native static placement, and CDN deployment."""
from __future__ import annotations
from pathlib import Path
from litestar import Litestar
from litestar_vite import (
DeployConfig,
PathConfig,
RuntimeConfig,
StaticPlacement,
StaticServerConfig,
StaticServerMount,
ViteConfig,
)
def create_production_config(bundle_dir: Path) -> ViteConfig:
"""Build a production ``ViteConfig`` with CDN prefix and Granian static offload."""
return ViteConfig(
mode="spa",
dev_mode=False,
paths=PathConfig(bundle_dir=bundle_dir, asset_url="https://cdn.example.com/assets/"),
deploy=DeployConfig(
enabled=True,
storage_backend="s3://my-app-assets-prod/assets",
asset_url="https://cdn.example.com/assets/",
delete_orphaned=True,
),
runtime=RuntimeConfig(start_dev_server=False),
)
def create_static_server_config() -> StaticServerConfig:
"""Configure Granian Rust-level static file serving with native placement."""
return StaticServerConfig(
placement=StaticPlacement.NATIVE, mounts=(StaticServerMount(route="/static", directory=Path("public/build")),)
)
def main() -> None:
"""Verify production ``ViteConfig`` and ``StaticServerConfig`` settings."""
cfg = create_production_config(Path("public/build"))
static_cfg = create_static_server_config()
assert cfg.dev_mode is False
assert cfg.asset_url == "https://cdn.example.com/assets/"
assert static_cfg.placement == StaticPlacement.NATIVE
assert Litestar is not None
__all__ = ("create_production_config", "create_static_server_config", "main")
Server & Static Placement Matrix¶
| ASGI Server | Static Serving Mechanism | Metadata Blocking (manifest.json, hot) |
Immutable Cache Headers |
|---|---|---|---|
Granian (StaticPlacement.NATIVE) |
Rust HTTP worker serves static files before Python/ASGI | Router hooks preserve StaticPlacement.NATIVE; configure equivalent server/CDN rules for native hits |
Configure at server/CDN layer for native hits |
Uvicorn / Hypercorn (StaticPlacement.ASGI) |
Litestar create_static_files_router |
Enforced via _StaticBeforeRequestHook (404 Not Found) |
Enforced via _StaticAfterRequestHook |
Asset Headers, Preloading, SRI, and CSP¶
Production HTML and asset responses served by Litestar apply browser-facing performance and security optimizations controlled by RuntimeConfig and ViteAssetLoader:
| Setting | Behavior |
|---|---|
immutable_cache_headers=True |
Files under the Vite assets directory whose names carry a Rollup content hash (index-D8x9kL2p.js) receive Cache-Control: public, max-age=31536000, immutable. Unhashed files (favicon.ico, icon-192x192.png) are untouched. |
link_preload_headers=True |
HTML responses include an RFC 8288 Link header with rel=modulepreload for the served entry and its transitive chunks and rel=preload; as=style for their stylesheets. Computed once per process from manifest.json. |
early_hints=True (opt-in) |
Sends the same preload links as a 103 Early Hints frame when the ASGI server advertises http.response.early_hint in scope["extensions"] (for example Hypercorn). Servers without the extension receive only the Link header. |
csp_nonce="..." |
Inline and entry scripts carry nonce="...". Inertia and template responses also honor a per-request request.scope["state"]["csp_nonce"] set by middleware. |
Manifest integrity (SRI) |
When your Vite build populates integrity hashes in manifest.json (e.g. via vite-plugin-manifest-sri), ViteAssetLoader automatically propagates integrity="sha384-..." and crossorigin="anonymous" onto <script>, <link rel="stylesheet">, and <link rel="modulepreload"> tags. |
Internal build metadata (manifest.json, .vite/manifest.json, ssr-manifest.json, .litestar.json, and the hot file) returns 404 from the Litestar static route regardless of these settings.
Granian Native Static Placement (StaticPlacement.NATIVE)
The built-in asset hardening (immutable_cache_headers and 404 blocking for manifest.json / .vite/manifest.json / hot) is implemented as picklable router-level hooks (_StaticBeforeRequestHook, _StaticAfterRequestHook) so StaticFilesConfig.has_asgi_overrides() stays False and the bundle remains eligible for Granian native serving. Consequently, router hooks do not execute on Granian-native hits — configure equivalent cache headers and metadata blocking at the reverse proxy or CDN layer when relying on StaticPlacement.NATIVE.
Deploying Bundles to S3, GCS, or Cloudflare R2¶
When DeployConfig is configured (or --storage / VITE_DEPLOY_STORAGE is set), litestar assets deploy runs vite build (unless --no-build is passed), uploads your compiled bundle_dir to any fsspec storage backend, and prunes orphaned files:
litestar assets deploy --storage s3://my-app-assets-prod/assets --storage-option region_name=us-east-1
Use --dry-run in CI pull requests to preview which hashed files would be uploaded or deleted without mutating remote storage: