How to Deploy a React App to AWS S3 and CloudFront

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:

  1. Navigate to S3 in AWS Console
  2. Click “Create bucket”
  3. Enter a unique bucket name
  4. Choose your region
  5. Uncheck “Block all public access” (required for web hosting)
  6. 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.html enables 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:

  1. Navigate to CloudFront
  2. Click “Create Distribution”
  3. Origin Settings:
    • Origin Domain: Select your S3 bucket
    • Origin Path: Leave empty
    • Origin Access: Public
  4. Default Cache Behavior:
    • Viewer Protocol Policy: Redirect HTTP to HTTPS
    • Allowed HTTP Methods: GET, HEAD, OPTIONS
    • Cache Policy: CachingOptimized
  5. 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
## 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 NameDescription
AWS_ACCESS_KEY_IDIAM user access key
AWS_SECRET_ACCESS_KEYIAM user secret key
REACT_APP_API_URLAPI 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:

ServiceUsageEstimated Cost
S3 Storage100 MB$0.02/month
S3 Requests10,000$0.01/month
CloudFront10 GB transfer$1.20/month
Route 531 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

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)

Thank you for reading! If you have any feedback or comments, please send them to [email protected].