
August 15, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Serving static assets directly from your Laravel application server creates a bottleneck that degrades performance as traffic grows. A proper AWS CloudFront CDN setup for Laravel assets offloads CSS, JavaScript, images, and fonts to edge locations worldwide while keeping your origin secure. This guide walks through the complete production configuration using S3 as the origin with Origin Access Control, ensuring your private bucket remains inaccessible except through CloudFront.
I have implemented this exact architecture across multiple production Laravel applications, including legal-tech portals and eCommerce platforms where asset performance directly impacts user experience and conversion rates. For teams evaluating whether custom infrastructure makes sense versus managed alternatives, understanding these fundamentals helps inform decisions about hiring Laravel expertise or managing deployments internally. The pattern described here works identically whether you are deploying from Kathmandu or anywhere else globally.
How do you configure AWS CloudFront CDN setup for Laravel assets with S3?
The foundation of a secure CloudFront distribution is ensuring your S3 bucket never allows public access. In 2026, AWS Origin Access Control (OAC) has fully replaced the legacy Origin Access Identity (OAI) method for new distributions. OAC uses IAM policy conditions tied to the specific CloudFront distribution ID, providing tighter security boundaries than the older service principal approach.
Create the private S3 bucket
Your asset bucket must block all public access at the account and bucket level. Create it via CLI or console with Block Public Access enabled:
aws s3api create-bucket \
--bucket my-laravel-assets-prod \
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
aws s3api put-public-access-block \
--bucket my-laravel-assets-prod \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" Never enable static website hosting on this bucket. That feature is designed for public websites and conflicts with OAC's security model. Your assets will be served exclusively through CloudFront.
Configure the OAC bucket policy
After creating your CloudFront distribution (covered below), attach this bucket policy replacing the placeholder distribution ID:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-laravel-assets-prod/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
} This policy grants read access only when requests originate from your specific CloudFront distribution. Direct S3 URL access returns 403 Forbidden even if someone discovers the bucket name. I have seen this prevent accidental data exposure on client projects where developers temporarily disabled public access blocks during debugging and forgot to re-enable them.
Create the CloudFront distribution with OAC
Use the AWS CLI to create a distribution configured for Laravel's versioned asset structure. Save this JSON as cf-config.json:
{
"CallerReference": "laravel-assets-prod-2026",
"Origins": {
"Quantity": 1,
"Items": [{
"Id": "S3-my-laravel-assets-prod",
"DomainName": "my-laravel-assets-prod.s3.ap-south-1.amazonaws.com",
"S3OriginConfig": {
"OriginAccessIdentity": ""
},
"OriginAccessControlId": "YOUR_OAC_ID"
}]
},
"DefaultCacheBehavior": {
"TargetOriginId": "S3-my-laravel-assets-prod",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {"Quantity": 2, "Items": ["GET", "HEAD"]},
"CachedMethods": {"Quantity": 2, "Items": ["GET", "HEAD"]},
"Compress": true,
"CachePolicyId": "CACHE_POLICY_ID_FOR_VERSIONED_ASSETS",
"ResponseHeadersPolicyId": "SECURITY_HEADERS_POLICY_ID"
},
"PriceClass": "PriceClass_All",
"Enabled": true,
"HttpVersion": "http2and3"
} Create the OAC first using aws cloudfront create-origin-access-control, then reference its ID above. After deployment, update your Laravel .env:
ASSET_URL=https://d1234abcdef.cloudfront.net
AWS_BUCKET=my-laravel-assets-prod
AWS_DEFAULT_REGION=ap-south-1 What cache policy settings work best for Laravel Vite assets?
Laravel Vite generates content-hashed filenames like app-a1b2c3d4.js. When file contents change, the hash changes, creating a new URL. This immutability lets you set aggressive caching without risking stale assets. Getting this wrong is one of the most common mistakes I encounter when auditing production setups.
Create a custom cache policy
Do not use the managed CachingOptimized policy. It includes query strings in the cache key, which wastes cache space since Laravel's hashed assets never use meaningful query parameters. Create a custom policy:
- TTL: Minimum 1 year, Default 1 year, Maximum 1 year
- Cache Key Parameters: None (ignore query strings and headers)
- Compression: Enable Brotli and Gzip
- Accept-Encoding Header: Include in cache key (required for compression negotiation)
This configuration means every unique path gets cached indefinitely at edge locations. When you deploy new assets with different hashes, old files simply stop being requested and eventually expire from cache naturally.
Handle non-versioned files separately
Files like favicon.ico, robots.txt, or PWA manifests don't receive content hashes. These need shorter TTLs. Create a second cache behavior in your distribution matching paths like /favicon.ico or /*.txt with a TTL of 1 day, ordered before the default behavior. Alternatively, move these files to your Laravel public directory and serve them from your application server if they change frequently.
How do you integrate CloudFront with Laravel Vite builds?
Vite's build process and Laravel's filesystem abstraction must align with your CloudFront configuration. Misalignment here causes broken assets after deployment or forces unnecessary cache invalidations.
Configure Vite for CDN output
In your vite.config.js, ensure the base path matches what CloudFront expects:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
build: {
manifest: true,
outDir: 'public/build',
},
}); You do not need to set base in Vite config when using Laravel's ASSET_URL. Laravel's @vite directive automatically prepends the asset URL from your environment. Setting both causes double-prefixing errors that break asset loading.
Upload assets during deployment
After running npm run build, sync the public/build directory to S3. Using Deployer 7, which I use for most production Laravel deployments including several sister sites on shared EC2 infrastructure:
task('deploy:assets', function () {
runLocally('cd {{release_path}} && npm ci && npm run build');
runLocally('aws s3 sync public/build s3://my-laravel-assets-prod/build --delete --cache-control "public,max-age=31536000,immutable"');
}); The --delete flag removes old hashed files that are no longer referenced. The explicit Cache-Control header reinforces the CloudFront cache policy at the origin level, providing defense in depth. For teams comparing deployment approaches, my article on CI/CD pipeline setup covers alternative strategies.
Verify the integration
After deployment, inspect your page source. Asset tags should reference your CloudFront domain:
<link rel="preload" as="style" href="https://d1234abcdef.cloudfront.net/build/assets/app-a1b2c3d4.css" />
<script type="module" src="https://d1234abcdef.cloudfront.net/build/assets/app-e5f6g7h8.js"></script> If URLs still point to your application domain, verify that ASSET_URL is set correctly in the deployed environment and that you cleared Laravel's config cache with php artisan config:cache.
How does CloudFront compare to other CDN options for Laravel?
Choosing the right CDN depends on your traffic patterns, budget, and operational capacity. Having deployed Laravel applications across multiple providers for clients in Nepal and internationally, I have practical experience with the trade-offs each option presents.
| Criteria | AWS CloudFront | Cloudflare | BunnyCDN |
|---|---|---|---|
| Setup Complexity | High (IAM, OAC, policies) | Low (DNS proxy) | Medium (pull zone) |
| Private S3 Support | Native OAC | R2 or authenticated pull | Authenticated pull only |
| Free Tier | 1 TB/month, 10M requests | Generous free plan | $1 trial credit |
| Nepal Edge Presence | Kolkata + Mumbai | Kathmandu PoP | Mumbai + Delhi |
| Invalidation Cost | First 1,000/month free | Instant purge included | Included |
| Best For | AWS-native, high-security | Full-site acceleration | Budget-conscious, simple assets |
For Laravel applications already on AWS infrastructure, CloudFront provides the tightest integration and lowest latency between S3 and edge. If you are serving a full site (not just assets) and want DDoS protection plus DNS management, Cloudflare's proxy mode simplifies operations significantly. BunnyCDN offers excellent price-to-performance for pure asset delivery when budget constraints matter more than ecosystem integration.
On legal-tech portals handling sensitive document workflows, I consistently choose CloudFront with OAC because the security boundary is auditable and compliant. For marketing sites or directories where assets are less sensitive, Cloudflare's ease of use often wins. Understanding these trade-offs helps when planning website development budgets that include ongoing infrastructure costs.
How do you troubleshoot common CloudFront and Laravel asset issues?
Even with correct configuration, production issues arise. These are the problems I encounter most frequently when maintaining Laravel applications with CloudFront.
Assets return 403 after deployment
This usually indicates an OAC misconfiguration. Verify these in order:
- The bucket policy references the correct distribution ARN (not a wildcard)
- OAC is attached to the distribution's origin settings
- The bucket has Block Public Access enabled (required for OAC)
- You waited 5-10 minutes after policy changes for propagation
Check CloudWatch logs for the distribution. The sc-status field shows origin response codes. A 403 from S3 with a 403 to viewer confirms permission issues rather than missing files.
Old assets persist after redeployment
If users see outdated CSS or JS despite new hashes, check these:
- Build output committed: Ensure
npm run buildran before S3 sync and the manifest updated - Laravel cache cleared: Run
php artisan config:cacheandphp artisan view:cacheafter deploy - Browser cache: Hard refresh or test in incognito to rule out local caching
- Wrong manifest: Verify the deployed
public/build/manifest.jsoncontains new hashes
Never rely on CloudFront invalidations for routine deployments. They cost money, take time, and indicate a broken asset versioning strategy. With proper Vite hashing, invalidations should only occur for emergency rollbacks.
CORS errors on fonts or WebGL assets
Browsers enforce CORS for certain asset types even when served from the same CDN. Attach a Response Headers Policy to your distribution including:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD
Access-Control-Max-Age: 31536000 For stricter security, replace * with your application domain. This is especially relevant for legal-tech portals where document viewers may load PDF.js workers or signature pad libraries from the CDN.
Finalizing Your AWS CloudFront CDN Setup for Laravel Assets
A correctly configured AWS CloudFront CDN setup for Laravel assets delivers measurable performance improvements while maintaining security boundaries that protect your origin storage. The combination of private S3 buckets with OAC, immutable cache policies for versioned Vite outputs, and automated deployment synchronization creates a system that scales without manual intervention. Start with the architecture outlined here, validate each layer independently, and monitor CloudWatch metrics during your first production week to catch edge cases specific to your traffic patterns.
If you need help implementing this setup or optimizing an existing Laravel deployment, reach out to discuss your project requirements.

