This guide covers deploying the Financial Assistant React frontend to AWS S3 with CloudFront CDN.
Frontend is deployed as static files to S3, NOT as a Docker container.
The docker-compose.frontend.yml file is only for local testing before deploying to S3. In production:
- Frontend: S3 + CloudFront (static files)
- Backend: EC2 with Docker (see DEPLOYMENT_BACKEND.md)
See DOCKER_DEPLOYMENT.md for Docker testing options.
- AWS Account with S3 and CloudFront access
- AWS CLI installed and configured
- Domain name (optional, for custom domain)
- Backend API deployed and accessible on EC2
Users → CloudFront CDN → S3 Bucket (Static Website) → Backend API (EC2)
Navigate to your frontend directory and update the API endpoint:
cd frontendFind the API configuration file (usually in src/config.js or similar) and update:
// src/config.js or where your API URL is defined
const API_BASE_URL = process.env.VITE_API_URL || 'https://api.your-domain.com';
// OR if you're using nginx proxy
const API_BASE_URL = 'https://your-backend-domain.com';
export default {
apiUrl: API_BASE_URL,
wsUrl: API_BASE_URL.replace('https', 'wss').replace('http', 'ws') + '/ws/chat'
};Create .env.production:
nano .env.productionAdd:
VITE_API_URL=https://your-backend-domain.com# Install dependencies
npm install
# Build for production
npm run buildThis creates an optimized build in the dist/ directory.
npm run previewVisit http://localhost:4173 to verify the build works.
# Replace with your bucket name
BUCKET_NAME="finassistant-frontend"
REGION="us-east-1"
# Create bucket
aws s3 mb s3://$BUCKET_NAME --region $REGION# Enable static website hosting
aws s3 website s3://$BUCKET_NAME --index-document index.html --error-document index.htmlNote: We use index.html for error document to support React Router.
Create a file bucket-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::finassistant-frontend/*"
}
]
}Replace finassistant-frontend with your actual bucket name.
Apply the policy:
aws s3api put-bucket-policy --bucket $BUCKET_NAME --policy file://bucket-policy.jsonaws s3api put-public-access-block \
--bucket $BUCKET_NAME \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"From your frontend directory:
# Upload all files from dist/
aws s3 sync dist/ s3://$BUCKET_NAME --delete
# Set proper content types and cache headers
aws s3 sync dist/ s3://$BUCKET_NAME \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "index.html" \
--exclude "*.json"
# Upload index.html with no cache (for updates)
aws s3 cp dist/index.html s3://$BUCKET_NAME/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html"Get the S3 website endpoint:
echo "http://$BUCKET_NAME.s3-website-$REGION.amazonaws.com"Visit this URL to test your site.
Create cloudfront-config.json:
{
"CallerReference": "finassistant-frontend-1",
"Comment": "Financial Assistant Frontend CDN",
"DefaultRootObject": "index.html",
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "S3-finassistant-frontend",
"DomainName": "finassistant-frontend.s3.amazonaws.com",
"S3OriginConfig": {
"OriginAccessIdentity": ""
}
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "S3-finassistant-frontend",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"],
"CachedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
}
},
"Compress": true,
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
}
},
"MinTTL": 0,
"DefaultTTL": 86400,
"MaxTTL": 31536000
},
"CustomErrorResponses": {
"Quantity": 1,
"Items": [
{
"ErrorCode": 404,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 300
}
]
},
"Enabled": true,
"PriceClass": "PriceClass_100"
}Or create via AWS Console (Recommended for first-time setup):
-
Go to CloudFront Console
-
Click "Create Distribution"
-
Configure:
- Origin Domain: Select your S3 bucket
- Origin Access: Legacy access identities (create new OAI)
- Viewer Protocol Policy: Redirect HTTP to HTTPS
- Allowed HTTP Methods: GET, HEAD
- Compress Objects: Yes
- Price Class: Use all edge locations (or select based on needs)
- Alternate Domain Names (CNAMEs): Your custom domain (if any)
- SSL Certificate: Request or import certificate via ACM
- Default Root Object:
index.html
-
Click "Create Distribution"
In CloudFront distribution settings:
- Go to "Error Pages" tab
- Create custom error response:
- HTTP Error Code: 404
- Customize Error Response: Yes
- Response Page Path:
/index.html - HTTP Response Code: 200
This ensures React Router works properly.
After creating OAI in CloudFront, update your S3 bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAI",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity YOUR_OAI_ID"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::finassistant-frontend/*"
}
]
}Apply:
aws s3api put-bucket-policy --bucket $BUCKET_NAME --policy file://updated-bucket-policy.json# List distributions
aws cloudfront list-distributions --query 'DistributionList.Items[*].[Id,DomainName]' --output tableNote your CloudFront domain: d1234abcd.cloudfront.net
Important: Certificate must be in us-east-1 region for CloudFront.
# Request certificate
aws acm request-certificate \
--domain-name your-domain.com \
--subject-alternative-names www.your-domain.com \
--validation-method DNS \
--region us-east-1- Go to ACM Console in
us-east-1 - Click on the certificate
- Create the CNAME records in your DNS provider
- Wait for validation (can take 5-30 minutes)
- Go to CloudFront distribution
- Click "Edit"
- Add Alternate Domain Names (CNAMEs):
your-domain.com, www.your-domain.com - Select your SSL Certificate from ACM
- Save changes
In your DNS provider (Route53, Cloudflare, etc.):
For Route53:
# Create A record (Alias) pointing to CloudFront
# Use AWS Console or CLI to create Alias recordFor other DNS providers: Create CNAME records:
your-domain.com → d1234abcd.cloudfront.net
www.your-domain.com → d1234abcd.cloudfront.net
Create deploy.sh in the frontend directory:
#!/bin/bash
# Configuration
BUCKET_NAME="finassistant-frontend"
DISTRIBUTION_ID="YOUR_CLOUDFRONT_DISTRIBUTION_ID"
REGION="us-east-1"
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}Starting deployment...${NC}"
# Build
echo -e "${BLUE}Building production bundle...${NC}"
npm run build
if [ $? -ne 0 ]; then
echo "Build failed!"
exit 1
fi
# Upload to S3
echo -e "${BLUE}Uploading to S3...${NC}"
aws s3 sync dist/ s3://$BUCKET_NAME \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "index.html" \
--exclude "*.json"
aws s3 cp dist/index.html s3://$BUCKET_NAME/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html"
# Invalidate CloudFront cache
echo -e "${BLUE}Invalidating CloudFront cache...${NC}"
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*"
echo -e "${GREEN}Deployment complete!${NC}"
echo -e "Site URL: https://your-domain.com"Make it executable:
chmod +x deploy.shUsage:
./deploy.shUpdate your backend api/app.py:54-60 to allow your frontend domain:
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://your-domain.com",
"https://www.your-domain.com",
"https://d1234abcd.cloudfront.net" # CloudFront domain
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Rebuild and redeploy your backend:
# On EC2
cd ~/Finassistant
git pull
docker-compose build
docker-compose up -dCreate .github/workflows/deploy-frontend.yml:
name: Deploy Frontend to S3
on:
push:
branches: [ main ]
paths:
- 'frontend/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
working-directory: ./frontend
run: npm ci
- name: Build
working-directory: ./frontend
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
working-directory: ./frontend
run: |
aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "index.html"
aws s3 cp dist/index.html s3://${{ secrets.S3_BUCKET }}/index.html \
--cache-control "no-cache"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"Add these secrets to GitHub:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYS3_BUCKETCLOUDFRONT_DISTRIBUTION_IDVITE_API_URL
- Check browser console for errors
- Verify API URL is correct in environment variables
- Check CORS settings on backend
- Ensure CloudFront error pages are configured
- Verify backend CORS allows your frontend domain
- Check API URL in browser network tab
- Ensure SSL certificate is valid
- Test API directly:
curl https://your-backend/api/health
- Create invalidation:
aws cloudfront create-invalidation --distribution-id ID --paths "/*" - Wait 5-10 minutes for invalidation to complete
- Clear browser cache (Cmd+Shift+R or Ctrl+Shift+R)
- Verify CloudFront custom error page is set to redirect 404 to
/index.html - Check S3 website error document is set to
index.html
CloudFront automatically compresses (already enabled if you followed this guide).
# Install image optimization tools
npm install --save-dev vite-plugin-imagemin
# Add to vite.config.js
import viteImagemin from 'vite-plugin-imagemin';
export default defineConfig({
plugins: [
react(),
viteImagemin({
gifsicle: { optimizationLevel: 7 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
svgo: { plugins: [{ name: 'removeViewBox' }] }
})
]
});Vite does this automatically, but ensure you're using dynamic imports for large components:
const HeavyComponent = lazy(() => import('./HeavyComponent'));Vite automatically adds hashes to filenames. Our deployment script handles cache headers correctly.
Monthly costs (approximate):
- S3 Storage: $0.023/GB (~$0.05 for typical React app)
- S3 Requests: $0.005/1000 GET requests
- CloudFront: First 1TB transfer free (AWS Free Tier), then $0.085/GB
- Route53 (if used): $0.50/hosted zone + $0.40/million queries
Total: ~$1-5/month for small to medium traffic
Add Lambda@Edge or CloudFront Functions to add security headers:
function handler(event) {
var response = event.response;
response.headers = {
...response.headers,
'strict-transport-security': { value: 'max-age=63072000; includeSubdomains; preload' },
'x-content-type-options': { value: 'nosniff' },
'x-frame-options': { value: 'DENY' },
'x-xss-protection': { value: '1; mode=block' },
'referrer-policy': { value: 'same-origin' }
};
return response;
}Add Web Application Firewall to CloudFront for DDoS protection and rate limiting.
aws cloudfront update-distribution \
--id YOUR_DISTRIBUTION_ID \
--logging Bucket=your-logs-bucket.s3.amazonaws.com,Prefix=cloudfront/,Enabled=trueMonitor these CloudFront metrics:
- Requests
- Bytes Downloaded
- Error Rate
- Cache Hit Rate
aws cloudwatch put-metric-alarm \
--alarm-name high-error-rate \
--alarm-description "Alert when error rate > 5%" \
--metric-name 4xxErrorRate \
--namespace AWS/CloudFront \
--statistic Average \
--period 300 \
--evaluation-periods 2 \
--threshold 5 \
--comparison-operator GreaterThanThreshold- Setup monitoring and alerts
- Configure AWS WAF for security
- Implement CloudFront Functions for headers
- Setup automated backups
- Configure CI/CD pipeline
- Test disaster recovery procedures
For issues:
- Check CloudFront distribution logs
- Verify S3 bucket permissions
- Test API connectivity from frontend
- Review browser console for errors