Deployment
How to Deploy a React App to AWS S3 and CloudFront
A complete guide to deploying a React or Vite application to AWS S3 with CloudFront as the CDN. Covers bucket configuration, CloudFront distribution, SSL, cache invalidation, GitHub Actions automation, and custom domain setup.
Why S3 and CloudFront for React
S3 with CloudFront is the standard AWS pattern for hosting single-page applications (React, Vue, Svelte). S3 stores the built static files. CloudFront is a global CDN that serves them from edge locations close to your users, adds SSL, and handles cache headers. Together they are cheap (cents per month for low traffic), globally fast, infinitely scalable, and require no servers to manage.
This guide covers a Vite-built React application but the steps apply equally to any static frontend build output. Prerequisites: an AWS account, AWS CLI configured with credentials, a domain name in Route 53 or your DNS provider, and your React project using Vite or Create React App.
Create and Configure the S3 Bucket
Create an S3 bucket for the build output. Do NOT enable public access — CloudFront will access the bucket through an Origin Access Control, keeping the bucket private.
# Create the S3 bucket (replace with your bucket name and region)
aws s3api create-bucket --bucket myapp-frontend --region us-east-1
# Build your React app
npm run build
# Upload the build output to S3
aws s3 sync dist/ s3://myapp-frontend --deleteCreate a CloudFront Distribution
Create a CloudFront distribution in the AWS console. Key settings: Origin domain is your S3 bucket (not the website endpoint). Create an Origin Access Control so CloudFront has permission to read the bucket. For the Default Root Object set index.html. For the Error Pages, add a custom error response for 403 and 404 that serves /index.html with HTTP status 200 — this is required for React Router to work correctly on direct URL access and page refreshes.
// S3 bucket policy allowing CloudFront Origin Access Control
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::myapp-frontend/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}Add a Custom Domain and SSL
Request a free SSL certificate in AWS Certificate Manager (ACM). Certificates for CloudFront must be in the us-east-1 region regardless of where your bucket is. Request a certificate for yourdomain.com and www.yourdomain.com. Validate with DNS by adding the CNAME records ACM provides to your DNS provider. Once validated, add the certificate and your custom domain to the CloudFront distribution Alternate Domain Names (CNAMEs) settings.
In your DNS provider, create CNAME records pointing your domain and www subdomain to the CloudFront distribution domain (e.g., d1234abcd.cloudfront.net). If your domain is in Route 53, create an Alias A record instead of a CNAME for the apex domain.
Cache Headers and Invalidation
React and Vite builds use content hashing in filenames (main.a1b2c3d4.js). These hashed files can be cached indefinitely. The index.html file must not be cached because it references the latest hashed filenames. Configure cache headers during the S3 sync.
# Upload hashed JS/CSS files with long cache
aws s3 sync dist/ s3://myapp-frontend --exclude "index.html" --cache-control "public, max-age=31536000, immutable" --delete
# Upload index.html with no-cache
aws s3 cp dist/index.html s3://myapp-frontend/index.html --cache-control "no-cache, no-store, must-revalidate"
# Invalidate the CloudFront cache for index.html after each deploy
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/index.html"Automate Deployments with GitHub Actions
Create a GitHub Actions workflow that builds and deploys on every push to main. Use OIDC authentication to AWS instead of long-lived access keys for better security.
name: Deploy to S3 and CloudFront
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-deploy-role
aws-region: us-east-1
- name: Sync to S3
run: |
aws s3 sync dist/ s3://myapp-frontend --exclude "index.html" --cache-control "public, max-age=31536000, immutable" --delete
aws s3 cp dist/index.html s3://myapp-frontend/index.html --cache-control "no-cache, no-store, must-revalidate"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/index.html"FAQ
How do I fix 403 errors when refreshing a React page on CloudFront?
This happens because CloudFront tries to find a file at the URL path (e.g., /about) and gets a 403 from S3 when no such file exists. Fix it by adding a custom error response in the CloudFront distribution: for HTTP error code 403 (and optionally 404), return the response page /index.html with HTTP response code 200. React Router then handles the route on the client side. This is required for any React SPA that uses client-side routing.
How much does S3 plus CloudFront cost for a small React app?
For a small to medium traffic site (100,000 requests per month, 10GB data transfer), the cost is approximately $1-3 per month. S3 storage for a typical React build (5-20MB) is under $0.01 per month. CloudFront data transfer is $0.0085-0.012 per GB for the first 10TB. CloudFront HTTP requests are $0.0075 per 10,000. Compare this to a t3.small EC2 instance at ~$15 per month for the same traffic — S3 plus CloudFront is dramatically cheaper for static content.
Should I use S3 plus CloudFront or Vercel for a React app?
Vercel is faster to set up (connect GitHub repo, done) and handles CI/CD, preview deployments, and edge functions automatically. S3 plus CloudFront gives you more control, is cheaper at scale, and keeps everything inside your AWS account (no external vendor). Choose Vercel for fast iteration and simplicity. Choose S3 plus CloudFront when you need to keep your deployment inside AWS for compliance or cost reasons, or when you need advanced CloudFront features like Lambda@Edge functions.
How do I handle environment variables in a React S3 deployment?
Environment variables for Vite (VITE_ prefix) and Create React App (REACT_APP_ prefix) are baked into the build at build time. They are not secret — they are visible in the JavaScript bundle. Set them in your GitHub Actions workflow as environment variables during the npm run build step, or inject them from GitHub Secrets. Never put server-side secrets (database passwords, API secret keys) in React environment variables — only public values like API base URLs and public keys.
Related free tools
If you want to turn this topic into action, use one of ShortIQ's free tools for campaign planning, UTM structure, or QR distribution.
Continue Reading
Explore more guides on link shortener SaaS strategy, Bitly alternatives, and white label link management.
Free newsletter
Get new guides in your inbox
We publish practical guides on dev tooling, prompt engineering, marketing workflows, and deployment. No fluff — straight to the point.
No spam. Unsubscribe any time.
Was this article helpful?
Tell us if this guide solved the problem or what was still missing. We use this to improve the blog and only follow up if you explicitly allow it.