Deploying a React application to AWS provides a scalable, cost-effective hosting solution with global content delivery. This guide walks you through deploying your React app to Amazon S3 for storage and CloudFront for worldwide distribution, complete with HTTPS, custom domains, and CI/CD integration.
Why S3 and CloudFront?
This architecture offers compelling advantages:
- Cost-effective: Pay only for storage and bandwidth used (often under $1/month for small sites)
- Highly scalable: Handles traffic spikes automatically without configuration
- Global CDN: CloudFront’s 400+ edge locations ensure fast load times worldwide
- HTTPS included: Free SSL/TLS certificates via AWS Certificate Manager
- Reliable: 99.99% availability SLA from AWS[1]
Architecture Overview
[React App] → [S3 Bucket] → [CloudFront CDN] → [Users Worldwide]
↓
[Route 53 (Optional)]
[ACM Certificate]
Prerequisites
Before starting, ensure you have:
- AWS account (free tier available)
- AWS CLI installed and configured
- Node.js and npm installed
- React application ready to deploy
- (Optional) Domain name for custom URL
Part 1: Prepare Your React Application
Step 1: Optimize for Production
Update your package.json with build scripts:
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
Step 2: Build Your Application
npm run build
This creates an optimized production build in the build/ directory with:
- Minified JavaScript bundles
- Optimized CSS
- Compressed assets
- Service worker (if using PWA features)
Step 3: Configure Environment Variables
Create environment-specific configuration:
// src/config.js
const config = {
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000',
env: process.env.NODE_ENV,
};
export default config;
Build with environment variables:
REACT_APP_API_URL=https://api.example.com npm run build
Part 2: Set Up S3 Bucket
Step 1: Create S3 Bucket
Using AWS CLI:
# Create bucket (use unique name)
aws s3 mb s3://my-react-app-bucket --region us-east-1
## Enable versioning (optional but recommended)
aws s3api put-bucket-versioning \
--bucket my-react-app-bucket \
--versioning-configuration Status=Enabled
Or via AWS Console:
- Navigate to S3 in AWS Console
- Click “Create bucket”
- Enter a unique bucket name
- Choose your region
- Uncheck “Block all public access” (required for web hosting)
- Click “Create bucket”
Step 2: Configure Bucket for Static Hosting
## Enable static website hosting
aws s3 website s3://my-react-app-bucket \
--index-document index.html \
--error-document index.html
Note: Setting error document to
index.htmlenables client-side routing in React Router.
Step 3: Set Bucket Policy
Create bucket-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-react-app-bucket/*"
}
]
}
Apply the policy:
aws s3api put-bucket-policy \
--bucket my-react-app-bucket \
--policy file://bucket-policy.json
Step 4: Upload Your Build
## Upload files
aws s3 sync build/ s3://my-react-app-bucket \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "index.html" \
--exclude "service-worker.js"
## Upload index.html separately with no-cache
aws s3 cp build/index.html s3://my-react-app-bucket/index.html \
--cache-control "no-cache, no-store, must-revalidate"
Why different cache settings?
- Static assets (JS, CSS): Long cache (1 year) because filenames include content hashes
- index.html: No cache to ensure users get the latest version pointing to new assets
Part 3: Set Up CloudFront Distribution
Step 1: Create CloudFront Distribution
Using AWS CLI:
aws cloudfront create-distribution \
--origin-domain-name my-react-app-bucket.s3.us-east-1.amazonaws.com \
--default-root-object index.html
Or via Console:
- Navigate to CloudFront
- Click “Create Distribution”
- Origin Settings:
- Origin Domain: Select your S3 bucket
- Origin Path: Leave empty
- Origin Access: Public
- Default Cache Behavior:
- Viewer Protocol Policy: Redirect HTTP to HTTPS
- Allowed HTTP Methods: GET, HEAD, OPTIONS
- Cache Policy: CachingOptimized
- Distribution Settings:
- Alternate Domain Names (CNAMEs): Add your domain
- SSL Certificate: Request or import certificate
- Default Root Object:
index.html
Step 2: Configure Custom Error Pages
For React Router support, redirect 403/404 errors to index.html:
## Create error response config
cat > error-responses.json << EOF
{
"Items": [
{
"ErrorCode": 403,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
},
{
"ErrorCode": 404,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
}
]
}
EOF
Step 3: Request SSL Certificate (Optional but Recommended)
## Request certificate in us-east-1 (required for CloudFront)
aws acm request-certificate \
--domain-name example.com \
--subject-alternative-names www.example.com \
--validation-method DNS \
--region us-east-1
Validate the certificate by adding DNS records as instructed by AWS.
Part 4: Configure Custom Domain with Route 53
Step 1: Create Hosted Zone
aws route53 create-hosted-zone \
--name example.com \
--caller-reference $(date +%s)
Step 2: Create A Record for CloudFront
{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d123456abcdef.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}
]
}
Apply the change:
aws route53 change-resource-record-sets \
--hosted-zone-id YOUR_ZONE_ID \
--change-batch file://create-record.json
Part 5: Automate Deployment with GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy to AWS
on:
push:
branches: [main]
workflow_dispatch:
env:
AWS_REGION: us-east-1
S3_BUCKET: my-react-app-bucket
CLOUDFRONT_DISTRIBUTION_ID: E1234567890ABC
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --watchAll=false
- name: Build application
env:
REACT_APP_API_URL: ${{ secrets.REACT_APP_API_URL }}
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Sync to S3
run: |
aws s3 sync build/ s3://${{ env.S3_BUCKET }} \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "index.html" \
--exclude "service-worker.js"
aws s3 cp build/index.html s3://${{ env.S3_BUCKET }}/index.html \
--cache-control "no-cache, no-store, must-revalidate"
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ env.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
Required GitHub Secrets
Add these secrets in your repository settings:
| Secret Name | Description |
|---|---|
AWS_ACCESS_KEY_ID | IAM user access key |
AWS_SECRET_ACCESS_KEY | IAM user secret key |
REACT_APP_API_URL | API endpoint URL |
Part 6: Implement Cache Invalidation Strategy
Selective Invalidation
Instead of invalidating all files (/*), invalidate only what changed:
## Invalidate only index.html and service worker
aws cloudfront create-invalidation \
--distribution-id E1234567890ABC \
--paths "/index.html" "/service-worker.js"
Cost Optimization
CloudFront includes 1,000 free invalidations per month[2]. Beyond that:
- Use versioned filenames (Create React App does this automatically)
- Only invalidate entry points (
index.html) - Consider scheduled invalidations for batching
Security Best Practices
1. Restrict S3 Bucket Access
Use Origin Access Identity (OAI) to restrict S3 access only through CloudFront:
## Create OAI
aws cloudfront create-cloud-front-origin-access-identity \
--cloud-front-origin-access-identity-config \
CallerReference="my-react-app-$(date +%s)",Comment="React App OAI"
Update S3 bucket policy to allow only CloudFront:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity YOUR_OAI_ID"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-react-app-bucket/*"
}
]
}
2. Enable Security Headers
Add security headers via CloudFront Functions:
function handler(event) {
var response = event.response;
var headers = response.headers;
headers['strict-transport-security'] = {
value: 'max-age=63072000; includeSubdomains; preload'
};
headers['x-content-type-options'] = { value: 'nosniff' };
headers['x-frame-options'] = { value: 'DENY' };
headers['x-xss-protection'] = { value: '1; mode=block' };
headers['referrer-policy'] = { value: 'same-origin' };
return response;
}
3. Enable CloudFront Logging
aws cloudfront update-distribution \
--id E1234567890ABC \
--distribution-config '{
"Logging": {
"Enabled": true,
"IncludeCookies": false,
"Bucket": "my-logs-bucket.s3.amazonaws.com",
"Prefix": "cloudfront/"
}
}'
Cost Estimation
For a typical React app with moderate traffic:
| Service | Usage | Estimated Cost |
|---|---|---|
| S3 Storage | 100 MB | $0.02/month |
| S3 Requests | 10,000 | $0.01/month |
| CloudFront | 10 GB transfer | $1.20/month |
| Route 53 | 1 hosted zone | $0.50/month |
| Total | ~$1.73/month |
Note: Costs scale with traffic. Free tier includes 50 GB CloudFront transfer and 1 million HTTP requests for 12 months[3].
Troubleshooting Common Issues
Issue: 403 Access Denied
Solution: Check bucket policy and ensure files are publicly readable.
Issue: Old version still showing
Solution: Clear browser cache and create CloudFront invalidation.
Issue: React Router 404s
Solution: Ensure CloudFront error responses redirect to index.html.
Issue: Mixed content warnings
Solution: Use HTTPS URLs for all external resources and APIs.
Monitoring and Analytics
CloudWatch Metrics
Monitor your distribution:
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name Requests \
--dimensions Name=DistributionId,Value=E1234567890ABC \
--start-time 2025-11-10T00:00:00Z \
--end-time 2025-11-11T00:00:00Z \
--period 3600 \
--statistics Sum
Key Metrics to Monitor
- Requests: Total number of requests
- BytesDownloaded: Total data transferred
- 4xxErrorRate: Client errors
- 5xxErrorRate: Server errors
Related Articles
- Cloudflare Workers: Serverless Web Application
- Load Balancing Algorithms and Strategies
- AWS US-EAST-1 DynamoDB Outage
- Penetration Testing Reconnaissance
Conclusion
Deploying React apps to AWS S3 and CloudFront provides enterprise-grade hosting at a fraction of the cost of traditional servers. With global CDN, HTTPS included, and seamless scaling, it’s an ideal solution for modern web applications.
This guide covered the complete deployment pipeline—from building your app to automating deployments with CI/CD. By following these best practices for security, caching, and monitoring, you’ll have a production-ready deployment that performs well globally.
Start with the basic setup, then progressively add features like custom domains, automated deployments, and advanced caching strategies as your application grows.
References
[1] Amazon Web Services. (2024). Amazon S3 Service Level Agreement. Available at: https://aws.amazon.com/s3/sla/ (Accessed: November 2025)
[2] Amazon Web Services. (2024). CloudFront Pricing. Available at: https://aws.amazon.com/cloudfront/pricing/ (Accessed: November 2025)
[3] Amazon Web Services. (2024). AWS Free Tier. Available at: https://aws.amazon.com/free/ (Accessed: November 2025)