I recently optimized my app’s image delivery by moving from direct storage / Vercel handling to a scalable CDN setup using AWS. Here’s the complete step-by-step breakdown.
🧠 Problem
- Images were slow to load globally
- Vercel image optimization was becoming costly
- Direct S3 access wasn’t efficient or secure
🎯 Goal
Build a system like:
User → CloudFront (CDN) → S3 (Private Storage)
🏗️ Architecture
- Amazon S3 → stores images
- Amazon CloudFront → CDN layer
- Route 53 → DNS
- ACM (SSL) → HTTPS
⚙️ Step-by-Step Setup
1. Upload images to S3
Organize structure like:
- /banner
- /products
- /users
2. Create CloudFront Distribution
- Origin: S3 bucket
- Origin Access: ✅ Private (recommended)
- Cache: Default optimized settings
- Protocol: Redirect HTTP → HTTPS
3. Configure Custom Domain
- Add:
cdn.yourdomain.com - Attach SSL certificate (ACM)
- Create CNAME in Route 53 → CloudFront
4. Test CDN
Example:
https://cdn.yourdomain.com/banner/image.jpg
⚡ Performance Optimization (IMPORTANT)
Add Cache Headers in S3
Set:
Cache-Control: public, max-age=31536000, immutable
This ensures:
- 1-year caching
- Instant repeat loads
- Reduced AWS costs
Bulk Update (CLI)
aws s3 cp s3://your-bucket/banner/ s3://your-bucket/banner/ \
--recursive \
--metadata-directive REPLACE \
--cache-control "public, max-age=31536000, immutable"
Invalidate CloudFront Cache
After changes:
/*
🔥 Results
- ⚡ Faster global image delivery
- 💸 Reduced Vercel costs
- 🔒 Secure (private S3 via CloudFront)
- 📈 Scalable for production
⚠️ Lessons Learned
- Don’t use root domain (use subdomain like cdn.domain.com)
- Always invalidate cache after metadata changes
- Never reuse image filenames (use versioning)
🧩 Bonus (Next.js Config)
module.exports = {
images: {
domains: ['cdn.yourdomain.com'],
unoptimized: true
}
};
🎉 Conclusion
S3 + CloudFront is one of the most cost-efficient and scalable ways to serve images in production. Once set up, it just works.
If you're building a startup or scaling your app — this setup is a must.
Happy building 🚀
Top comments (0)