Deploy React on AWS with GitHub Actions

⚛️ React ☁️ Amazon Web Services ⚙️ GitHub Actions

Configuration Files

4 files
Production-ready configuration files with detailed comments and best practices. Each file works together as a complete deployment solution.
Complete AWS infrastructure for S3 + CloudFront hosting
terraform
# Terraform configuration for React on AWS S3 + CloudFront
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  default = "us-east-1"
}

variable "app_name" {
  default = "my-react-app"
}

variable "domain_name" {
  description = "Custom domain (optional)"
  default     = ""
}

# S3 Bucket for static website
resource "aws_s3_bucket" "website" {
  bucket = "${var.app_name}-website"
}

resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html"  # SPA routing
  }
}

resource "aws_s3_bucket_public_access_block" "website" {
  bucket = aws_s3_bucket.website.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.website.arn}/*"
      }
    ]
  })

  depends_on = [aws_s3_bucket_public_access_block.website]
}

# CloudFront Origin Access Identity
resource "aws_cloudfront_origin_access_identity" "website" {
  comment = "OAI for ${var.app_name}"
}

# CloudFront Distribution
resource "aws_cloudfront_distribution" "website" {
  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"  # US, Canada, Europe

  origin {
    domain_name = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id   = "S3-${var.app_name}"

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.website.cloudfront_access_identity_path
    }
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-${var.app_name}"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
    compress               = true
  }

  # SPA routing - return index.html for 404s
  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  custom_error_response {
    error_code         = 403
    response_code      = 200
    response_page_path = "/index.html"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

# Outputs
output "s3_bucket_name" {
  value = aws_s3_bucket.website.id
}

output "cloudfront_distribution_id" {
  value = aws_cloudfront_distribution.website.id
}

output "cloudfront_domain_name" {
  value = aws_cloudfront_distribution.website.domain_name
}

output "website_url" {
  value = "https://${aws_cloudfront_distribution.website.domain_name}"
}

Pro Tips

  • 💰 S3 costs ~$0.023/GB/month, CloudFront ~$0.085/GB for first 10TB
  • 🌍 PriceClass_100 covers US, Canada, Europe (cheapest option)
  • ⚡ CloudFront caching reduces S3 requests by 90%+
  • 🔒 HTTPS enabled by default with CloudFront certificate
  • 🎯 SPA routing: 404s redirect to index.html for client-side routing
  • 💡 Run "terraform apply" to create all infrastructure
  • 📊 Add custom domain by updating viewer_certificate block
  • ⚠️ Remember "terraform destroy" when done testing
Vite build configuration optimized for AWS deployment
typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
        // Cache busting with content hashes
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]'
      },
    },
  },
})

Pro Tips

  • 📦 Content hashes enable long-term caching (1 year)
  • ⚡ Vendor chunk separates React from app code
  • 🗺️ Source maps help debug production issues
  • 💡 CloudFront caches assets with immutable headers
  • 🎯 Manual chunks improve cache hit rates
Project dependencies and deployment scripts
json
{
  "name": "my-react-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "deploy": "npm run build && aws s3 sync dist/ s3://my-react-app-website --delete"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.20.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "typescript": "^5.2.2",
    "vite": "^5.0.8"
  }
}

Pro Tips

  • 🚀 "deploy" script for manual deployments
  • 📦 react-router-dom for client-side routing
  • ⚡ TypeScript type-checks before building
  • 💡 Update S3 bucket name in deploy script
  • 🎯 Use GitHub Actions for automated deployments
Environment variables for AWS deployment
bash
# AWS Configuration (for local deployment script)
AWS_REGION=us-east-1
AWS_PROFILE=default

# S3 Bucket (created by Terraform)
S3_BUCKET=my-react-app-website

# CloudFront Distribution ID (from Terraform output)
CLOUDFRONT_DISTRIBUTION_ID=E1234567890ABC

# Public environment variables (baked into build)
VITE_API_URL=https://api.example.com
VITE_APP_NAME=My React App
VITE_ENABLE_ANALYTICS=true

Pro Tips

  • 🔒 Never commit .env to git (add to .gitignore)
  • 💡 VITE_ prefix makes variables available in browser
  • ⚠️ All VITE_ variables are PUBLIC (in bundle)
  • 🎯 Use AWS Secrets Manager for sensitive data
  • 📝 Copy to .env.local for local development
  • 🚀 GitHub Actions uses repository secrets instead

Prerequisites

  • AWS account with billing enabled
  • S3 bucket configured for static hosting
  • CloudFront distribution created
  • IAM credentials with appropriate permissions

Deployment Steps

  • Create S3 bucket with static website hosting enabled
  • Create CloudFront distribution pointing to S3 bucket
  • Create IAM user with S3 and CloudFront permissions
  • Add AWS credentials to GitHub Secrets
  • Create .github/workflows/deploy.yml with the configuration
  • Push to trigger deployment

Additional Notes

  • Consider egress fees for high-traffic sites
  • CloudFront provides global CDN
  • Enable S3 versioning for rollback capability