diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical
index ab1d6fbb7..9302578fe 100644
--- a/Dockerfile.fips.standalone-infisical
+++ b/Dockerfile.fips.standalone-infisical
@@ -185,6 +185,9 @@ COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build
+# Make export-assets script executable for CDN asset extraction
+RUN chmod +x /backend/scripts/export-assets.sh
+
ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical
index 0674a00e9..faf489b2d 100644
--- a/Dockerfile.standalone-infisical
+++ b/Dockerfile.standalone-infisical
@@ -174,6 +174,9 @@ ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build
+# Make export-assets script executable for CDN asset extraction
+RUN chmod +x /backend/scripts/export-assets.sh
+
ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
diff --git a/backend/package.json b/backend/package.json
index 0e17bb2b7..4f9cfdc97 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -25,6 +25,7 @@
"outputPath": "binary"
},
"scripts": {
+ "assets:export": "./scripts/export-assets.sh",
"binary:build": "npm run binary:clean && npm run build:frontend && npm run build && npm run binary:babel-frontend && npm run binary:babel-backend && npm run binary:rename-imports",
"binary:package": "pkg --no-bytecode --public-packages \"*\" --public --target host .",
"binary:babel-backend": " babel ./dist -d ./dist",
diff --git a/backend/scripts/export-assets.sh b/backend/scripts/export-assets.sh
new file mode 100644
index 000000000..0a1ee3763
--- /dev/null
+++ b/backend/scripts/export-assets.sh
@@ -0,0 +1,77 @@
+#!/bin/sh
+# Export frontend static assets for CDN deployment
+# Usage:
+# npm run assets:export - Output tar to stdout (pipe to file or aws s3)
+# npm run assets:export /path - Extract assets to specified directory
+# npm run assets:export -- --help - Show usage
+
+set -e
+
+ASSETS_PATH="/backend/frontend-build/assets"
+
+show_help() {
+ cat << 'EOF'
+Export frontend static assets for CDN deployment.
+
+USAGE:
+ docker run --rm infisical/infisical npm run assets:export [-- OPTIONS] [PATH]
+
+OPTIONS:
+ --help, -h Show this help message
+
+ARGUMENTS:
+ PATH Directory to export assets to. If not provided, outputs
+ a tar archive to stdout.
+
+EXAMPLES:
+ # Export as tar to local file
+ docker run --rm infisical/infisical npm run assets:export > assets.tar
+
+ # Extract to local directory
+ docker run --rm -v $(pwd)/cdn-assets:/output infisical/infisical npm run assets:export /output
+
+ # Pipe directly to S3 (using aws cli on host)
+ docker run --rm infisical/infisical npm run assets:export | \
+ tar -xf - -C ./tmp-assets && \
+ aws s3 sync ./tmp-assets s3://my-bucket/assets
+
+EOF
+ exit 0
+}
+
+# Check for help flag
+case "${1:-}" in
+ --help|-h)
+ show_help
+ ;;
+esac
+
+# Verify assets exist
+if [ ! -d "$ASSETS_PATH" ]; then
+ echo "Error: Assets directory not found at $ASSETS_PATH" >&2
+ echo "Make sure the frontend is built and included in the image." >&2
+ exit 1
+fi
+
+ASSET_COUNT=$(find "$ASSETS_PATH" -type f | wc -l | tr -d ' ')
+
+if [ $# -eq 0 ]; then
+ # No path provided - output tar to stdout
+ echo "Exporting $ASSET_COUNT assets as tar archive to stdout..." >&2
+ tar -cf - -C "$(dirname "$ASSETS_PATH")" "$(basename "$ASSETS_PATH")"
+else
+ # Path provided - extract to directory
+ OUTPUT_PATH="$1"
+
+ if [ ! -d "$OUTPUT_PATH" ]; then
+ echo "Creating output directory: $OUTPUT_PATH" >&2
+ mkdir -p "$OUTPUT_PATH"
+ fi
+
+ echo "Exporting $ASSET_COUNT assets to $OUTPUT_PATH..." >&2
+ cp -r "$ASSETS_PATH"/* "$OUTPUT_PATH/"
+
+ echo "✅ Assets exported successfully!" >&2
+ echo " Path: $OUTPUT_PATH" >&2
+ echo " Files: $ASSET_COUNT assets" >&2
+fi
diff --git a/docs/docs.json b/docs/docs.json
index be76a10c1..83e44bb05 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -330,6 +330,7 @@
"self-hosting/guides/custom-certificates",
"self-hosting/guides/automated-bootstrapping",
"self-hosting/guides/production-hardening",
+ "self-hosting/guides/cdn-caching",
"self-hosting/guides/monitoring-telemetry"
]
},
diff --git a/docs/self-hosting/guides/cdn-caching.mdx b/docs/self-hosting/guides/cdn-caching.mdx
new file mode 100644
index 000000000..cc4c97887
--- /dev/null
+++ b/docs/self-hosting/guides/cdn-caching.mdx
@@ -0,0 +1,106 @@
+---
+title: "CDN Caching for Static Assets"
+description: "How to set up CDN caching to prevent version skew issues during deployments"
+---
+
+This guide explains a common issue with frontend asset caching during deployments and how to solve it using a CDN.
+
+## The Problem: Version Skew
+
+Modern frontend build tools like Vite generate content-hashed filenames for static assets (e.g., `main-abc123.js`). Each build produces unique filenames based on file contents. During deployments, this can cause a race condition:
+
+1. User loads `index.html` which references `main-abc123.js`
+2. New deployment replaces containers with a new build
+3. New containers only serve `main-xyz789.js` (new build)
+4. User's browser requests `main-abc123.js` from cached HTML
+5. Request returns **404** — the old asset no longer exists
+
+This results in broken pages, failed SPA navigation, and requires users to manually refresh.
+
+
+This is a documented limitation in Vite's official guidance: [Load Error Handling](https://vite.dev/guide/build#load-error-handling)
+
+
+### Current Behavior
+
+Infisical includes a built-in workaround that detects version mismatches and triggers a page reload. While functional, this introduces a noticeable delay for users during deployments.
+
+## The Solution: External Asset Storage
+
+The solution is to store static assets externally (e.g., S3, GCS, Azure Blob) and serve them through a CDN (e.g., CloudFront, Cloud CDN, Cloudflare). Assets are uploaded **before** container deployment, ensuring old versions remain available.
+
+### How It Works
+
+```mermaid
+flowchart LR
+ User[User Browser]
+ CDN[CDN]
+ S3[(Object Storage)]
+ App[Your Infrastructure]
+
+ User --> CDN
+ CDN -->|"/assets/*"| S3
+ CDN -->|"/* (default)"| App
+```
+
+The key points:
+
+- **Asset persistence**: Old assets remain available even after new deployments
+- **Deployment order**: Upload new assets before deploying new containers
+- **Long cache TTL**: Content-hashed files can be cached indefinitely (we recommend 30 days)
+- **Automatic cleanup**: Configure lifecycle rules to expire old assets after 30 days
+
+At Infisical, we use **CloudFront + S3** for this purpose, but you can use any CDN and object storage combination that fits your infrastructure.
+
+## Exporting Assets
+
+Infisical provides a built-in command to export frontend assets from the Docker image:
+
+```bash
+# Export as tar archive to stdout
+docker run --rm infisical/infisical npm run assets:export > assets.tar
+
+# Extract the archive
+tar -xf assets.tar
+ls assets/ # Content-hashed JS/CSS files
+```
+
+Or export directly to a mounted directory:
+
+```bash
+docker run --rm -v $(pwd)/cdn-assets:/output \
+ infisical/infisical npm run assets:export /output
+```
+
+### What Gets Exported
+
+The command exports the `/assets` directory containing:
+
+- JavaScript bundles (e.g., `main-abc123.js`, `chunk-def456.js`)
+- CSS files (e.g., `styles-789xyz.css`)
+- Other static assets with content hashes
+
+These files are safe to cache with long TTLs because their filenames change whenever the content changes.
+
+## Integration with Your Pipeline
+
+The general deployment flow should be:
+
+1. **Build** your new Docker image (or pull the official Infisical image)
+2. **Export** assets using `npm run assets:export`
+3. **Upload** assets to your object storage
+4. **Deploy** the new container version
+
+```bash
+# Example: Export and upload to S3
+docker run --rm infisical/infisical:$VERSION npm run assets:export > assets.tar
+tar -xf assets.tar
+aws s3 sync assets s3://your-bucket/assets --cache-control "public, max-age=2592000"
+
+# Then deploy your container
+```
+
+
+Always upload assets **before** deploying the new container. This ensures the assets referenced by the new `index.html` exist before users can access them.
+
+