You’ve learned the individual pieces: creating a bucket, uploading files, writing a bucket policy, enabling static website hosting, and configuring versioning. Now put them all together. In this exercise, you’ll deploy a complete static site to S3 and access it in a browser.
If the console labels move while you’re doing this exercise, keep the S3 static website tutorial and the aws s3 sync command reference open. AWS changes the UI more often than the core concepts.
Why It Matters
Every frontend deployment is fundamentally the same: take build output, put it somewhere accessible, and point a URL at it. Vercel and Netlify hide this process behind a git push. Doing it manually on S3 teaches you what actually happens under the hood—and gives you the foundation for the automated pipeline you’ll build later in this course.
Set Up Your Static Site
Create a minimal static site on your local machine. You don’t need a framework—plain HTML, CSS, and JavaScript are enough.
Create a directory called build/ with these three files:
build/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My S3 Static Site</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<h1>Hello from S3</h1>
<p>This site is hosted entirely on Amazon S3.</p>
<p id="timestamp"></p>
</div>
<script src="app.js"></script>
</body>
</html>build/styles.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #0a0a0a;
color: #ededed;
}
.container {
text-align: center;
max-width: 600px;
padding: 2rem;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
color: #999;
margin-bottom: 0.5rem;
}build/app.js
document.getElementById('timestamp').textContent =
'Page loaded at: ' + new Date().toLocaleTimeString();build/error.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Page Not Found</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: #0a0a0a;
color: #ededed;
}
.error {
text-align: center;
}
h1 {
font-size: 4rem;
}
a {
color: #4a9eff;
}
</style>
</head>
<body>
<div class="error">
<h1>404</h1>
<p>This page does not exist.</p>
<p><a href="/">Go back home</a></p>
</div>
</body>
</html>Create the Bucket
Create an S3 bucket. Remember that bucket names are globally unique—if my-frontend-app-assets is taken, add a unique suffix.
aws s3 mb s3://my-frontend-app-assets \
--region us-east-1Checkpoint
Run aws s3 ls --region us-east-1 and confirm your bucket appears in the list.
Disable Block Public Access
Since you’re hosting a public website, disable Block Public Access:
aws s3api put-public-access-block \
--bucket my-frontend-app-assets \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false" \
--region us-east-1Checkpoint
Run the following and confirm all four settings are false:
aws s3api get-public-access-block \
--bucket my-frontend-app-assets \
--region us-east-1 \
--output jsonApply a Bucket Policy
Apply a bucket policy that allows public read access:
aws s3api put-bucket-policy \
--bucket my-frontend-app-assets \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-frontend-app-assets/*"
}
]
}' \
--region us-east-1Checkpoint
Run aws s3api get-bucket-policy --bucket my-frontend-app-assets --region us-east-1 --output json and confirm the policy is in place.
Upload Your Files
Sync your local build/ directory to the bucket:
aws s3 sync ./build s3://my-frontend-app-assets \
--region us-east-1Checkpoint
Run aws s3 ls s3://my-frontend-app-assets/ --region us-east-1 and confirm you see all four files: index.html, styles.css, app.js, and error.html.
Expected output:
2026-03-18 12:00:00 456 app.js
2026-03-18 12:00:00 612 error.html
2026-03-18 12:00:00 534 index.html
2026-03-18 12:00:00 423 styles.cssEnable Static Website Hosting
Configure the bucket for static website hosting:
aws s3 website s3://my-frontend-app-assets/ \
--index-document index.html \
--error-document error.htmlCheckpoint
Run the following and confirm the index and error documents are set:
aws s3api get-bucket-website \
--bucket my-frontend-app-assets \
--region us-east-1 \
--output jsonExpected output:
{
"IndexDocument": {
"Suffix": "index.html"
},
"ErrorDocument": {
"Key": "error.html"
}
}View Your Site
Open this URL in your browser (substitute your actual bucket name):
http://my-frontend-app-assets.s3-website-us-east-1.amazonaws.comYou should see the “Hello from S3” page with a timestamp showing the current time. The CSS should be applied (dark background, centered text) and the JavaScript should be running (timestamp updates on each page load).
Checkpoint
- The page loads and displays “Hello from S3”
- The CSS is applied (dark background, light text)
- The timestamp appears at the bottom
- Navigating to a nonexistent path (like
/nonexistent) shows your custom 404 page
Enable Versioning
Enable versioning so you’re protected against accidental overwrites:
aws s3api put-bucket-versioning \
--bucket my-frontend-app-assets \
--versioning-configuration Status=Enabled \
--region us-east-1Checkpoint
aws s3api get-bucket-versioning \
--bucket my-frontend-app-assets \
--region us-east-1 \
--output jsonExpected output:
{
"Status": "Enabled"
}Deploy an Update
Make a change to build/index.html—update the <h1> to say “Hello from S3 (v2)“. Then redeploy:
aws s3 sync ./build s3://my-frontend-app-assets \
--region us-east-1Refresh your browser. You should see the updated heading. Now list the versions of index.html:
aws s3api list-object-versions \
--bucket my-frontend-app-assets \
--prefix "index.html" \
--region us-east-1 \
--output jsonCheckpoint
You should see two versions of index.html—the original and the updated one. The IsLatest field on the newer version should be true.
Failure Diagnosis
- The website endpoint returns
403 Forbidden: One of the three public-hosting pieces is missing. Check Block Public Access, the bucket policy, and the website hosting configuration in that order. - Your browser loads HTML with broken styles or JavaScript: The files either were not synced from
build/or the object keys do not match the filenames referenced inindex.html. - You uploaded the update but still see the old page: Hard-refresh the browser or open the site in a private window. With raw S3 website hosting, browser caching is usually the culprit.
Stretch Goals
- Add a lifecycle rule that deletes noncurrent versions after 7 days. Verify it with
get-bucket-lifecycle-configuration. - Test a rollback. Download the previous version of
index.htmlusing its version ID, then re-upload it withaws s3 cp. Confirm the site reverts to the original content. - Add a subdirectory. Create a
build/about/index.htmlpage and verify that navigating to/about/(with trailing slash) serves the page. - Check content types. Run
aws s3api head-object --bucket my-frontend-app-assets --key "styles.css" --region us-east-1 --output jsonand verify the content type istext/css.