How to Deploy a React App to AWS S3 and CloudFront: Step-by-Step Guide

Looking for a reliable, scalable, and cost-efficient way to deploy your React app to AWS? While services like AWS Amplify offer one-click deployments, hosting your React app directly on S3 + CloudFront gives you full control, predictable pricing, blazing-fast global delivery, and a setup that scales to millions of users for just a few dollars per month.

In this practical guide, we walk you through the entire process from building your React production bundle to serving it via a custom domain with HTTPS. No fluff, no shortcuts, just a real-world deployment you can replicate today.

Why Deploy a React App to AWS S3 and CloudFront?

Before diving in, here is why this stack remains the go-to choice in 2026 for serious React deployments:

  • Cost: Hosting a small to mid-traffic SPA typically costs less than $1 per month.
  • Performance: CloudFront serves your files from 600+ edge locations globally.
  • Scalability: S3 handles any traffic spike without configuration.
  • Security: Free SSL via AWS Certificate Manager and integration with AWS WAF.
  • Control: Unlike Amplify, you decide every part of the pipeline.

S3 + CloudFront vs Other AWS Options

Option Best For Monthly Cost (low traffic)
S3 + CloudFront Static SPAs, full control ~$0.50 to $2
AWS Amplify Git-based CI/CD, beginners ~$5 to $15
AWS App Runner Containerized SSR apps ~$25+
EC2 Custom server needs ~$10+
aws cloud server

Prerequisites

  • An AWS account with billing enabled
  • Node.js 20+ installed locally
  • The AWS CLI v2 installed and a domain name (optional for custom domain)
  • A React app (we will create one with Vite, today’s de-facto standard)

Step 1: Build Your React App

If you don’t already have a React app, create one using Vite:

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run build

This generates a dist/ folder containing your production-ready static assets (HTML, JS, CSS). These are the files we will upload to S3.

Step 2: Create an IAM User for Deployments

Never use root credentials. Create a dedicated IAM user with the minimum permissions required.

  1. Go to IAM > Users > Create user
  2. Name it react-deployer
  3. Attach a custom policy with these permissions:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetObject"],
      "Resource": [
        "arn:aws:s3:::your-bucket-name",
        "arn:aws:s3:::your-bucket-name/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": ["cloudfront:CreateInvalidation"],
      "Resource": "*"
    }
  ]
}

Generate an access key for programmatic access, then configure the AWS CLI:

aws configure --profile react-deployer
aws cloud server

Step 3: Create and Configure the S3 Bucket

In the AWS Console, go to S3 > Create bucket:

  • Bucket name: choose a unique name (for example myapp-prod-2026)
  • Region: pick the one closest to your origin (CloudFront caches globally anyway)
  • Block all public access: keep it ENABLED (CloudFront will access it via OAC)

Once created, upload your dist/ folder contents:

aws s3 sync ./dist s3://myapp-prod-2026 --profile react-deployer --delete

Step 4: Create a CloudFront Distribution

This is where the magic of global delivery happens.

  1. Go to CloudFront > Create distribution
  2. Origin domain: select your S3 bucket from the dropdown
  3. Origin access: choose Origin access control settings (recommended) and create a new OAC
  4. Viewer protocol policy: Redirect HTTP to HTTPS
  5. Allowed HTTP methods: GET, HEAD
  6. Default root object: index.html
  7. Price class: choose based on your audience (Use Only North America and Europe is cheapest)

After creating the distribution, CloudFront will show a bucket policy snippet. Click Copy policy and paste it in your S3 bucket under Permissions > Bucket policy.

Handle React Router (Client-Side Routing)

If you use React Router, refreshing on /dashboard returns a 403 from S3. Fix it with a CloudFront Function or custom error response:

  • Go to your distribution > Error pages
  • Create custom error response for 403: response page path /index.html, HTTP response code 200
  • Do the same for 404

Step 5: Configure a Custom Domain with HTTPS

Request a Free SSL Certificate

  1. Open AWS Certificate Manager in the us-east-1 region (mandatory for CloudFront)
  2. Request a public certificate for yourdomain.com and www.yourdomain.com
  3. Validate via DNS by adding the provided CNAME records to your DNS provider

Attach the Domain to CloudFront

  1. Edit your CloudFront distribution settings
  2. Add your domain under Alternate domain names (CNAMEs)
  3. Select your newly issued ACM certificate
  4. Save changes

Point Your DNS

In Route 53 (or your DNS provider), create an A record (Alias) pointing to your CloudFront distribution domain (something like d123abc.cloudfront.net).

aws cloud server

Step 6: Automate Deploys with Cache Invalidation

CloudFront caches files at the edge. After a new deploy, you must invalidate the cache so users see the latest version.

Create a deploy.sh script:

#!/bin/bash
set -e

npm run build

aws s3 sync ./dist s3://myapp-prod-2026 \
  --profile react-deployer \
  --delete \
  --cache-control "public, max-age=31536000, immutable" \
  --exclude "index.html"

aws s3 cp ./dist/index.html s3://myapp-prod-2026/index.html \
  --profile react-deployer \
  --cache-control "public, max-age=0, must-revalidate" \
  --content-type "text/html"

aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/index.html" \
  --profile react-deployer

echo "Deployment complete"

Why this strategy works:

  • Hashed JS/CSS files (created by Vite) get a 1-year cache, perfect for performance
  • index.html is never cached, so users always pull the latest references
  • You only invalidate one file, keeping you within the free 1,000 invalidation paths per month

Run it:

chmod +x deploy.sh
./deploy.sh

Step 7 (Bonus): CI/CD with GitHub Actions

Store your IAM credentials as GitHub secrets, then add .github/workflows/deploy.yml:

name: Deploy to AWS
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - 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: us-east-1
      - run: aws s3 sync ./dist s3://myapp-prod-2026 --delete
      - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} --paths "/index.html"

Common Pitfalls to Avoid

  • Wrong ACM region: certificates for CloudFront must be in us-east-1
  • Public S3 bucket: keep it private and use OAC instead
  • Forgetting cache invalidation: users will see stale builds for up to 24 hours
  • Wrong content-type: explicitly set text/html for index.html when using aws s3 cp
  • Mixing up SPA routing: configure 403/404 error pages to return index.html

FAQ

How much does it cost to deploy a React app on AWS S3 and CloudFront?

For a typical small to medium SPA with a few thousand monthly visitors, expect to pay between $0.50 and $2 per month. CloudFront includes 1 TB of free data transfer per month under the AWS Free Tier (always-free) since late 2024.

Is S3 + CloudFront better than AWS Amplify?

It depends on priorities. Amplify is faster to set up with Git-based CI/CD. S3 + CloudFront is cheaper, more configurable, and better for production workloads where you control every layer.

Can I use this method for Next.js?

Only if you export a fully static Next.js site (output: 'export'). For SSR or ISR Next.js apps, use AWS Amplify, OpenNext on Lambda, or App Runner instead.

How do I add environment variables?

Vite and Create React App bundle environment variables at build time. Set them in your CI environment (GitHub Actions secrets) before running npm run build. They will be inlined into the static bundle.

Do I need Route 53 for the custom domain?

No. Any DNS provider (Cloudflare, Namecheap, GoDaddy) works. You just create a CNAME or ALIAS record pointing to your CloudFront domain. Route 53 simply integrates more tightly with AWS.

How do I rollback a bad deployment?

Enable S3 versioning on your bucket. To rollback, restore the previous version of each file, then invalidate /index.html. For zero-downtime rollbacks, consider blue/green deployments using two CloudFront origins.

Wrapping Up

You now have a production-grade pipeline to deploy your React app to AWS using S3 and CloudFront. Your app is fast, secure with HTTPS, served globally from the edge, and costs almost nothing to run. Once your deploy.sh or GitHub Actions workflow is in place, shipping new versions takes seconds.

At adproductstogo.com, this is the exact pattern we use to ship lightweight, blazing-fast frontends to our clients worldwide. If you need help architecting your AWS deployment or building scalable React applications, get in touch with our team.