From repository to a public URL#
The frontend is what your users see: components, styles and interaction logic. Deploying it well means every commit on the right branch ends up published at the right URL, repeatably, without anyone copying files by hand.
Any frontend deployment platform asks for roughly the same inputs: the repository, a DNS name, how to build the project, whether content is static or server-rendered, and a path that answers so you know the site is alive. This guide shows how to cover those same pieces directly with AWS services.
The first decision matters most: does your build output static files, or does it need a server on every request?
Static or SSR: pick the architecture#
If your build command outputs a folder of HTML, CSS and JavaScript the browser loads as-is (a Vite SPA or a Next.js static export), your site is static. Amazon S3 can store it but does not run server-side code. If each request needs a Node.js process to render HTML, you need compute.
| Option | Good fit for | What you manage |
|---|---|---|
| Private S3 + CloudFront (OAC) | Static sites and SPAs | Bucket, distribution, policy, CI pipeline |
| AWS Amplify Hosting | Static, SPA and SSR frameworks; Git-based flow | Build settings and branches; AWS handles the rest |
| Container on ECS (for example ECS Express Mode) | SSR with custom runtime or network needs | Image, service, load balancer and scaling |
This guide details the first option, the most common one for SPAs, and summarizes Amplify. The container route is covered in the guide on deploying a containerized HTTP service on AWS.
Documentation: Amazon S3 · Static website hosting ↗ · Vite · Static deploy ↗ · Next.js · Static exports ↗ · Amazon ECS · Express Mode ↗
Private S3 with CloudFront and OAC#
The recommended pattern is an S3 bucket with no public access (Block Public Access on) and a CloudFront distribution in front that reads from it through origin access control (OAC). AWS recommends OAC; origin access identity (OAI) is the older mechanism and is marked legacy. The bucket policy only lets that specific distribution read:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}- Set index.html as the distribution's default root object. Note that it only applies to the root, not to subdirectories.
- For an SPA with client-side routes, add custom error responses that return /index.html with status 200 for 403 and 404. With OAC and no s3:ListBucket permission, S3 answers 403 (not 404) when an object does not exist.
- Serve your own domain with an ACM certificate and a Route 53 alias pointing to the distribution.
Documentation: CloudFront · Restrict access to S3 (OAC) ↗ · Amazon S3 · Block Public Access ↗ · CloudFront · Default root object ↗ · CloudFront · Change response codes ↗ · Amazon S3 · GetObject and 403 ↗
GitHub Actions pipeline with OIDC#
The pipeline builds on every push to main, gets temporary AWS credentials through OIDC (no access keys stored in GitHub), uploads the files and invalidates what must not stay cached:
# .github/workflows/deploy-frontend.yml
name: deploy-frontend
on:
push:
branches: [main]
permissions:
id-token: write # required to request the OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build # outputs dist/
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::111122223333:role/frontend-deploy
aws-region: us-east-1
# Hashed asset names: long cache
- run: >
aws s3 sync dist/ s3://amzn-s3-demo-bucket/ --delete
--exclude index.html
--cache-control "public,max-age=31536000,immutable"
# index.html: always revalidate
- run: >
aws s3 cp dist/index.html s3://amzn-s3-demo-bucket/index.html
--cache-control "no-cache"
- run: >
aws cloudfront create-invalidation
--distribution-id EDFDVBD6EXAMPLE --paths "/index.html"The caching strategy avoids mass invalidations. Bundlers emit hashed file names, so those files can be cached for a year; index.html is served with no-cache and is the only file invalidated. AWS documents this as using versioned file names instead of invalidating.
Documentation: GitHub · OIDC with AWS ↗ · configure-aws-credentials ↗ · AWS CLI · s3 sync ↗ · AWS CLI · create-invalidation ↗ · CloudFront · Invalidation vs versioned names ↗ · CloudFront · Content expiration ↗
Managed alternative: Amplify Hosting#
Amplify Hosting provides a Git-based workflow with continuous deployment: you connect the repository, Amplify builds on every push and publishes to its CDN. It supports SSR frameworks, SPAs and static site generators.
Each connected branch becomes its own deployment with its own URL (for example main and dev), which maps nicely to one environment per branch. It is the option with the fewest moving parts when you need SSR and do not want to run containers; in exchange, you get less control over the infrastructure than with S3 and CloudFront.
Documentation: AWS Amplify Hosting ↗ · Amplify · Branch deployments ↗
Verify the deployment#
- Open the public URL, go straight to an internal route (for example /profile) and reload: if you see the app rather than an S3 XML error, the error responses are set up correctly.
- Check that the bucket does not answer on its direct endpoint; it should only serve through CloudFront.
- Inspect the Cache-Control headers of index.html and of a hashed asset.
- Define a simple health path (the root is usually enough) that returns 200 and use it in your monitoring.
Checklist#
- Reproducible build (npm ci, pinned Node version).
- Private bucket, OAC, and a policy scoped to the distribution.
- 403/404 mapped to /index.html only for SPAs.
- CI credentials via OIDC with least privilege; no secrets in the client bundle.
- Long cache for hashed assets; no-cache for index.html.
Sources and scope
Documentation checked on September 25, 2026. Examples and decision criteria are editorial proposals; adapt them to your application's contract and validate them in an authorized test environment.
- Amazon S3 · Static website hosting ↗
- Vite · Static deploy ↗
- Next.js · Static exports ↗
- Amazon ECS · Express Mode ↗
- CloudFront · Restrict access to S3 (OAC) ↗
- Amazon S3 · Block Public Access ↗
- CloudFront · Default root object ↗
- CloudFront · Change response codes ↗
- Amazon S3 · GetObject and 403 ↗
- GitHub · OIDC with AWS ↗
- configure-aws-credentials ↗
- AWS CLI · s3 sync ↗
- AWS CLI · create-invalidation ↗
- CloudFront · Invalidation vs versioned names ↗
- CloudFront · Content expiration ↗
- AWS Amplify Hosting ↗
- Amplify · Branch deployments ↗
Compare cloud options
Review pricing, limits, conditions and sources for each option (in Spanish).
Open comparison