Deploy React SPA to AWS S3 + CloudFront (Step-by-Step)
Introduction
Deploying a React Single Page Application (SPA) to production requires careful consideration of hosting, performance, and scalability. AWS S3 combined with CloudFront provides one of the most cost-effective and performant solutions for hosting static React applications.
This comprehensive guide will walk you through the complete process of deploying your React SPA to AWS S3 with CloudFront CDN, including proper routing configuration for SPAs, SSL certificate setup, and performance optimization. By the end of this tutorial, you'll have a production-ready deployment pipeline that scales globally.
Why AWS S3 + CloudFront for React SPAs?
Benefits of This Architecture
The combination of AWS S3 and CloudFront offers several advantages for React applications:
// Architecture Benefits
const AwsArchitectureBenefits = {
cost: "Pay only for storage and data transfer used",
performance: "Global CDN with edge locations worldwide",
scalability: "Automatic scaling to handle traffic spikes",
security: "SSL/TLS encryption and AWS security features",
reliability: "99.99% uptime SLA with multiple availability zones",
simplicity: "No server management required",
};
// Cost comparison example
const CostComparison = {
traditionalHosting: "$20-100/month for VPS",
awsS3CloudFront: "$1-10/month for most applications",
savings: "Up to 90% cost reduction for static sites",
};
When to Choose This Solution
This deployment method is ideal for:
- React SPAs with client-side routing
- Static sites generated with Create React App, Vite, or similar tools
- Global applications requiring fast load times worldwide
- Cost-conscious projects with variable traffic patterns
- Scalable solutions that need to handle traffic spikes
Prerequisites
Before we begin, ensure you have:
# Required tools and accounts
✅ AWS Account with billing enabled
✅ AWS CLI installed and configured
✅ Node.js and npm/yarn installed
✅ React application ready for production
✅ Custom domain (optional but recommended)
Setting Up AWS CLI
If you haven't set up AWS CLI yet:
# Install AWS CLI (Windows)
winget install Amazon.AWSCLI
# Configure AWS CLI
aws configure
# AWS Access Key ID: [Your access key]
# AWS Secret Access Key: [Your secret key]
# Default region name: us-east-1
# Default output format: json
Step 1: Prepare Your React Application
Build for Production
First, ensure your React app is optimized for production:
# Navigate to your React project
cd your-react-app
# Install dependencies
npm install
# Create production build
npm run build
Optimize Build Configuration
Configure Public URL
For S3 deployment, update your package.json
:
{
"name": "your-react-app",
"version": "1.0.0",
"homepage": "https://your-domain.com",
"scripts": {
"build": "react-scripts build",
"deploy": "npm run build && aws s3 sync build/ s3://your-bucket-name --delete"
}
}
Environment Variables
Create a .env.production
file for production-specific configurations:
# .env.production
REACT_APP_API_URL=https://api.your-domain.com
REACT_APP_ENV=production
GENERATE_SOURCEMAP=false
Build Analysis
Analyze your build to ensure optimal bundle size:
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Analyze build
npx webpack-bundle-analyzer build/static/js/*.js
Step 2: Create and Configure S3 Bucket
Create S3 Bucket
# Create S3 bucket (bucket names must be globally unique)
aws s3 mb s3://your-unique-bucket-name --region us-east-1
# Example with timestamp
aws s3 mb s3://my-react-app-$(date +%s) --region us-east-1
Configure Bucket for Static Website Hosting
# Enable static website hosting
aws s3 website s3://your-bucket-name \
--index-document index.html \
--error-document error.html
Set Bucket Policy for Public Access
Create a bucket policy file bucket-policy.json
:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
Apply the policy:
# Apply bucket policy
aws s3api put-bucket-policy \
--bucket your-bucket-name \
--policy file://bucket-policy.json
# Disable block public access
aws s3api put-public-access-block \
--bucket your-bucket-name \
--public-access-block-configuration \
BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
Step 3: Deploy React App to S3
Upload Build Files
# Sync build folder to S3
aws s3 sync build/ s3://your-bucket-name --delete
# With specific cache settings
aws s3 sync build/ s3://your-bucket-name \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "*.html" \
--exclude "service-worker.js"
# HTML files with shorter cache
aws s3 sync build/ s3://your-bucket-name \
--cache-control "public, max-age=0, must-revalidate" \
--include "*.html" \
--include "service-worker.js"
Automated Deployment Script
Create a deployment script deploy.sh
:
#!/bin/bash
set -e
BUCKET_NAME="your-bucket-name"
DISTRIBUTION_ID="your-cloudfront-distribution-id"
echo "🏗️ Building React app..."
npm run build
echo "📤 Uploading to S3..."
# Upload with long cache for static assets
aws s3 sync build/ s3://$BUCKET_NAME \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "*.html" \
--exclude "service-worker.js" \
--exclude "asset-manifest.json"
# Upload HTML files with no cache
aws s3 sync build/ s3://$BUCKET_NAME \
--cache-control "public, max-age=0, must-revalidate" \
--include "*.html" \
--include "service-worker.js" \
--include "asset-manifest.json"
echo "🔄 Invalidating CloudFront..."
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*"
echo "✅ Deployment complete!"
Step 4: Set Up CloudFront Distribution
Create CloudFront Distribution
# Create distribution configuration
cat > cloudfront-config.json << EOF
{
"CallerReference": "$(date +%s)",
"DefaultRootObject": "index.html",
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "S3Origin",
"DomainName": "your-bucket-name.s3.us-east-1.amazonaws.com",
"S3OriginConfig": {
"OriginAccessIdentity": ""
}
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "S3Origin",
"ViewerProtocolPolicy": "redirect-to-https",
"MinTTL": 0,
"DefaultTTL": 86400,
"MaxTTL": 31536000,
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
}
},
"TrustedSigners": {
"Enabled": false,
"Quantity": 0
}
},
"Comment": "React SPA Distribution",
"Enabled": true,
"PriceClass": "PriceClass_All"
}
EOF
# Create distribution
aws cloudfront create-distribution \
--distribution-config file://cloudfront-config.json
Configure SPA Routing
For React Router to work properly, configure custom error pages:
# Update distribution for SPA routing
aws cloudfront update-distribution \
--id YOUR_DISTRIBUTION_ID \
--distribution-config '{
"CallerReference": "spa-routing-update",
"CustomErrorResponses": {
"Quantity": 2,
"Items": [
{
"ErrorCode": 404,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
},
{
"ErrorCode": 403,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
}
]
}
}'
Optimize Cache Behaviors
Create optimized cache behaviors for different file types:
{
"CacheBehaviors": {
"Quantity": 3,
"Items": [
{
"PathPattern": "*.js",
"TargetOriginId": "S3Origin",
"ViewerProtocolPolicy": "https-only",
"DefaultTTL": 31536000,
"Compress": true
},
{
"PathPattern": "*.css",
"TargetOriginId": "S3Origin",
"ViewerProtocolPolicy": "https-only",
"DefaultTTL": 31536000,
"Compress": true
},
{
"PathPattern": "*.html",
"TargetOriginId": "S3Origin",
"ViewerProtocolPolicy": "https-only",
"DefaultTTL": 0,
"Compress": true
}
]
}
}
Step 5: Configure Custom Domain and SSL
Request SSL Certificate
# Request certificate in us-east-1 (required for CloudFront)
aws acm request-certificate \
--domain-name your-domain.com \
--subject-alternative-names www.your-domain.com \
--validation-method DNS \
--region us-east-1
Validate Certificate
# Get certificate validation records
aws acm describe-certificate \
--certificate-arn your-certificate-arn \
--region us-east-1
Add the DNS validation records to your domain's DNS settings.
Update CloudFront with Custom Domain
# Update distribution with custom domain
aws cloudfront update-distribution \
--id YOUR_DISTRIBUTION_ID \
--distribution-config '{
"Aliases": {
"Quantity": 2,
"Items": ["your-domain.com", "www.your-domain.com"]
},
"ViewerCertificate": {
"ACMCertificateArn": "your-certificate-arn",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
}
}'
Update DNS Records
Point your domain to CloudFront:
# Create ALIAS record for apex domain
# your-domain.com -> d1234567890.cloudfront.net
# Create CNAME record for www subdomain
# www.your-domain.com -> d1234567890.cloudfront.net
Step 6: Implement CI/CD Pipeline
GitHub Actions Deployment
Create .github/workflows/deploy.yml
:
name: Deploy to AWS S3 + CloudFront
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
REACT_APP_API_URL: ${{ secrets.REACT_APP_API_URL }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "*.html" \
--exclude "service-worker.js"
aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} \
--cache-control "public, max-age=0, must-revalidate" \
--include "*.html" \
--include "service-worker.js"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
Environment Variables Setup
Add these secrets to your GitHub repository:
# GitHub repository settings > Secrets and variables > Actions
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
S3_BUCKET=your-bucket-name
CLOUDFRONT_DISTRIBUTION_ID=your-distribution-id
REACT_APP_API_URL=https://api.your-domain.com
Step 7: Performance Optimization
Enable Compression
Ensure CloudFront compression is enabled:
{
"DefaultCacheBehavior": {
"Compress": true,
"ViewerProtocolPolicy": "redirect-to-https"
}
}
Optimize React Build
Code Splitting
Implement route-based code splitting:
// App.js
import { lazy, Suspense } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
// Lazy load components
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
const Contact = lazy(() => import("./pages/Contact"));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
Bundle Analysis and Optimization
// webpack-bundle-analyzer setup
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
reportFilename: "bundle-report.html",
}),
],
};
CloudFront Optimization
Custom Cache Headers
# Set appropriate cache headers for different file types
aws s3 cp build/ s3://your-bucket/ --recursive \
--cache-control "public, max-age=31536000, immutable" \
--include "*.js" --include "*.css" --include "*.woff*" --include "*.png" --include "*.jpg"
aws s3 cp build/ s3://your-bucket/ --recursive \
--cache-control "public, max-age=0, must-revalidate" \
--include "*.html" --include "service-worker.js"
HTTP/2 and GZIP
CloudFront automatically enables:
- HTTP/2 for improved multiplexing
- GZIP compression for text-based assets
- Brotli compression for better compression ratios
Step 8: Monitoring and Analytics
CloudWatch Metrics
Monitor your deployment with CloudWatch:
# Get CloudFront metrics
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name Requests \
--dimensions Name=DistributionId,Value=YOUR_DISTRIBUTION_ID \
--start-time 2023-01-01T00:00:00Z \
--end-time 2023-01-02T00:00:00Z \
--period 3600 \
--statistics Sum
Real User Monitoring
Implement performance monitoring in your React app:
// Performance monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";
function sendToAnalytics(metric) {
// Send to your analytics service
console.log(metric);
}
// Measure Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Error Tracking
Set up error boundaries and tracking:
// ErrorBoundary.js
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error: error,
errorInfo: errorInfo,
});
// Log error to monitoring service
console.error("Error caught by boundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>
We apologize for the inconvenience. Please try refreshing the page.
</p>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Troubleshooting Common Issues
SPA Routing Problems
Problem: 404 errors on direct URL access or page refresh.
Solution: Ensure CloudFront custom error responses are configured:
{
"CustomErrorResponses": {
"Items": [
{
"ErrorCode": 404,
"ResponsePagePath": "/index.html",
"ResponseCode": "200"
},
{
"ErrorCode": 403,
"ResponsePagePath": "/index.html",
"ResponseCode": "200"
}
]
}
}
Cache Issues
Problem: Updates not reflected immediately.
Solution: Create CloudFront invalidation:
# Invalidate all files
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
# Invalidate specific files
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/index.html" "/static/js/*"
CORS Issues
Problem: API calls failing due to CORS.
Solution: Configure CORS in your S3 bucket:
{
"CORSRules": [
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "POST", "PUT", "DELETE"],
"AllowedOrigins": ["https://your-domain.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
}
SSL Certificate Issues
Problem: Certificate not validating or not applying.
Solution: Ensure certificate is in us-east-1
region and properly validated:
# Check certificate status
aws acm describe-certificate \
--certificate-arn your-certificate-arn \
--region us-east-1
# List certificates in us-east-1
aws acm list-certificates --region us-east-1
Cost Optimization
S3 Storage Classes
Optimize costs with appropriate storage classes:
# Configure lifecycle policy for old versions
aws s3api put-bucket-lifecycle-configuration \
--bucket your-bucket-name \
--lifecycle-configuration '{
"Rules": [{
"ID": "DeleteOldVersions",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"NoncurrentVersionExpiration": {
"NoncurrentDays": 30
}
}]
}'
CloudFront Pricing
Choose appropriate price class:
{
"PriceClass": "PriceClass_100", // Use only US, Canada, Europe
"PriceClass": "PriceClass_200", // Add Asia, Australia
"PriceClass": "PriceClass_All" // Global distribution (highest cost)
}
Monitoring Costs
Set up billing alerts:
# Create billing alarm
aws cloudwatch put-metric-alarm \
--alarm-name "AWS-Billing-Alert" \
--alarm-description "Alert when AWS bill exceeds $10" \
--metric-name EstimatedCharges \
--namespace AWS/Billing \
--statistic Maximum \
--period 86400 \
--threshold 10 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=Currency,Value=USD
Advanced Configuration
Multi-Environment Setup
Manage multiple environments (dev, staging, prod):
# Environment-specific deployment
#!/bin/bash
ENVIRONMENT=${1:-dev}
BUCKET_NAME="myapp-${ENVIRONMENT}"
DISTRIBUTION_ID=$(aws cloudfront list-distributions \
--query "DistributionList.Items[?Comment=='${ENVIRONMENT}'].Id" \
--output text)
echo "Deploying to ${ENVIRONMENT} environment..."
# Build with environment-specific config
REACT_APP_ENV=${ENVIRONMENT} npm run build
# Deploy to environment-specific bucket
aws s3 sync build/ s3://${BUCKET_NAME} --delete
# Invalidate environment-specific distribution
aws cloudfront create-invalidation \
--distribution-id ${DISTRIBUTION_ID} \
--paths "/*"
Security Headers
Implement security headers with Lambda@Edge:
// security-headers.js (Lambda@Edge function)
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
headers["strict-transport-security"] = [
{
key: "Strict-Transport-Security",
value: "max-age=31536000; includeSubDomains",
},
];
headers["content-security-policy"] = [
{
key: "Content-Security-Policy",
value: "default-src 'self'; script-src 'self' 'unsafe-inline'",
},
];
headers["x-content-type-options"] = [
{
key: "X-Content-Type-Options",
value: "nosniff",
},
];
headers["x-frame-options"] = [
{
key: "X-Frame-Options",
value: "DENY",
},
];
headers["referrer-policy"] = [
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
];
callback(null, response);
};
Blue-Green Deployment
Implement zero-downtime deployments:
#!/bin/bash
# Blue-Green deployment script
BLUE_BUCKET="myapp-blue"
GREEN_BUCKET="myapp-green"
CURRENT_ORIGIN=$(aws cloudfront get-distribution \
--id $DISTRIBUTION_ID \
--query 'Distribution.DistributionConfig.Origins.Items[0].DomainName' \
--output text)
if [[ $CURRENT_ORIGIN == *"blue"* ]]; then
TARGET_BUCKET=$GREEN_BUCKET
echo "Current: Blue, Deploying to: Green"
else
TARGET_BUCKET=$BLUE_BUCKET
echo "Current: Green, Deploying to: Blue"
fi
# Deploy to target bucket
aws s3 sync build/ s3://$TARGET_BUCKET --delete
# Update CloudFront origin
aws cloudfront update-distribution \
--id $DISTRIBUTION_ID \
--distribution-config "$(aws cloudfront get-distribution-config \
--id $DISTRIBUTION_ID \
--query 'DistributionConfig' \
--output json | \
jq ".Origins.Items[0].DomainName = \"$TARGET_BUCKET.s3.amazonaws.com\"")"
echo "Switched to $TARGET_BUCKET"
Best Practices and Security
Security Checklist
✅ Use HTTPS only (redirect HTTP to HTTPS)
✅ Implement proper CORS policies
✅ Use least privilege IAM policies
✅ Enable CloudTrail logging
✅ Set up proper bucket policies
✅ Use AWS WAF for additional protection
✅ Implement security headers
✅ Regular security audits
Performance Checklist
✅ Enable GZIP/Brotli compression
✅ Implement proper caching strategies
✅ Use code splitting and lazy loading
✅ Optimize images and assets
✅ Minimize bundle sizes
✅ Use HTTP/2 features
✅ Implement service workers for caching
✅ Monitor Core Web Vitals
Deployment Checklist
✅ Test build locally before deployment
✅ Verify all environment variables
✅ Check routing configuration
✅ Test SSL certificate
✅ Validate DNS settings
✅ Monitor deployment metrics
✅ Set up alerts and monitoring
✅ Document rollback procedures
Conclusion
Deploying a React SPA to AWS S3 with CloudFront provides a scalable, cost-effective, and performant hosting solution. This setup offers:
- Global Performance: CloudFront's edge locations ensure fast load times worldwide
- Cost Efficiency: Pay only for what you use with S3 and CloudFront pricing
- Scalability: Automatic scaling to handle any amount of traffic
- Security: SSL/TLS encryption and AWS security features
- Reliability: High availability with AWS's infrastructure
By following this comprehensive guide, you now have:
- Production-ready deployment process
- Automated CI/CD pipeline with GitHub Actions
- Performance optimization techniques
- Monitoring and troubleshooting strategies
- Security best practices implementation
Next Steps
Consider implementing these advanced features:
- API Gateway integration for backend APIs
- AWS Amplify for more integrated development workflow
- Lambda@Edge for advanced request/response processing
- AWS WAF for additional security
- Multi-region deployment for disaster recovery
With this foundation, your React application is ready to scale and perform optimally in production. The combination of S3 and CloudFront provides enterprise-grade hosting that can handle everything from small projects to large-scale applications serving millions of users.
Remember to regularly monitor your application's performance, update dependencies, and review your AWS costs to ensure optimal operation over time.