Host a Static Website on S3: A Step-by-Step Production Guide
You have a polished HTML/CSS site sitting on your local machine — getting it live shouldn't require a server, a DevOps team, or a monthly bill. Amazon S3's Static Website Hosting feature turns a storage bucket into a globally accessible web endpoint in under 10 minutes.
TL;DR
| Step | Action | Key Detail |
|---|---|---|
| 1 | Create S3 Bucket | Name must match your domain if using Route 53; choose your target region |
| 2 | Disable Block Public Access | Account-level AND bucket-level settings must both allow public access |
| 3 | Enable Static Website Hosting | Set index document (index.html) and optional error document |
| 4 | Attach a Bucket Policy | Grant s3:GetObject to Principal: "*" for public reads |
| 5 | Upload Your Files | Maintain relative paths; set correct Content-Type metadata |
| 6 | (Optional) Add CloudFront | HTTPS, custom domain, global CDN edge caching |
Architecture Overview
Before diving into configuration, understand the request flow from a visitor's browser to your S3-hosted content.
(Optional)"] EP["S3 Website
Endpoint"] BP["Bucket Policy
s3:GetObject"] OBJ["📄 S3 Object
index.html / style.css"] Browser -->|"HTTP GET"| CF CF -->|"Origin Request"| EP Browser -->|"Direct HTTP GET
(without CloudFront)"| EP EP -->|"Evaluate Policy"| BP BP -->|"Allow: Principal *"| OBJ OBJ -->|"200 OK + Content"| Browser
- Browser issues an HTTP GET to the S3 website endpoint URL.
- S3 Website Endpoint (distinct from the REST API endpoint) handles the request — it understands index documents and error routing.
- S3 evaluates the Bucket Policy to confirm
s3:GetObjectis permitted for the anonymous principal. - The matching object (e.g.,
index.html) is returned with its storedContent-Type. - Optionally, CloudFront sits in front to provide HTTPS termination, a custom domain, and edge caching — S3 becomes the origin.
Analogy: Think of S3 as a self-service library. The bucket is the building, each object is a book on the shelf, and the bucket policy is the sign on the door that says "Open to the public — anyone may read." Static Website Hosting is simply flipping the sign from "Private Archive" to "Public Reading Room" and designating which book is handed to visitors who don't ask for a specific title (index.html).
Step 1 — Create the S3 Bucket
Navigate to the S3 console and create a new bucket. Two critical decisions here:
- Region: Choose the AWS region geographically closest to your primary audience to minimize latency. If you plan to use CloudFront later, this matters less.
- Bucket Name: If you intend to use a custom domain via Route 53 with an Alias record pointing directly to the S3 website endpoint, the bucket name must exactly match the domain name (e.g.,
www.example.com).
Leave all other settings at their defaults for now — we'll handle public access in the next step.
Step 2 — Disable Block Public Access
AWS enables "Block Public Access" by default at both the account level and the bucket level. You must disable the relevant settings at both layers for a public bucket policy to take effect.
Via AWS Console
Go to your bucket → Permissions tab → Block public access (bucket settings) → Edit → Uncheck "Block all public access" → Save.
Via AWS CLI
# Disable block public access at the BUCKET level
aws s3api put-public-access-block \
--bucket YOUR_BUCKET_NAME \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"
⚠️ Also verify your account-level setting hasn't been locked down by your AWS Organization or account admin:
aws s3control get-public-access-block --account-id 123456789012
Step 3 — Enable Static Website Hosting
This is the feature that transforms S3 from a raw object store into a web server. It activates a dedicated website endpoint and enables index/error document routing.
Via AWS Console
Bucket → Properties tab → scroll to Static website hosting → Edit → Enable → Set Index document to index.html → Set Error document to error.html (optional) → Save.
Via AWS CLI
aws s3 website s3://YOUR_BUCKET_NAME/ \
--index-document index.html \
--error-document error.html
After saving, S3 will display your website endpoint URL in the format:
http://YOUR_BUCKET_NAME.s3-website-REGION.amazonaws.com
Note this URL — it's HTTP only. HTTPS requires CloudFront (covered in Step 6).
Step 4 — Attach a Public Bucket Policy
Enabling the feature isn't enough — you must explicitly grant read permission to anonymous users via a bucket policy. The policy below follows least-privilege: it grants only s3:GetObject (read), scoped to all objects in this specific bucket.
🔽 Click to expand — Public Read Bucket Policy (JSON)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
}
]
}
Apply it via CLI:
aws s3api put-bucket-policy \
--bucket YOUR_BUCKET_NAME \
--policy file://bucket-policy.json
ARN breakdown: arn:aws:s3:::YOUR_BUCKET_NAME/* — S3 is a global service, so the region and account-id fields are intentionally empty. The /* wildcard scopes the permission to every object inside the bucket, not the bucket itself.
Step 5 — Upload Your Website Files
Use the AWS CLI sync command for efficient, incremental uploads. It only transfers new or changed files.
# Sync your local ./public directory to the bucket root
aws s3 sync ./public s3://YOUR_BUCKET_NAME/ --delete
The --delete flag removes objects in S3 that no longer exist locally, keeping your bucket in sync with your source. Ensure your index.html is at the bucket root.
Content-Type tip: The AWS CLI infers Content-Type from file extensions automatically. If you upload files without extensions, set it explicitly:
aws s3 cp ./sitemap \
s3://YOUR_BUCKET_NAME/sitemap \
--content-type "application/xml"
Step 6 (Optional but Recommended) — Add CloudFront for HTTPS
The S3 website endpoint is HTTP-only. For production use, HTTPS is non-negotiable for security and SEO. CloudFront solves this with zero server management.
Alias Record"] CDN["CloudFront
Distribution
(HTTPS + Custom Domain)"] CERT["ACM Certificate
us-east-1"] S3["S3 Bucket
(Private)"] OAC["Origin Access
Control (OAC)"] User -->|"HTTPS Request"| DNS DNS -->|"Routes to"| CDN CERT -->|"TLS Termination"| CDN CDN -->|"Via OAC"| OAC OAC -->|"Authenticated Fetch"| S3 S3 -->|"Object Response"| CDN CDN -->|"Cached HTTPS Response"| User
- Create a CloudFront Distribution with your S3 website endpoint as the origin (use the website endpoint URL, not the REST API endpoint, to preserve index document routing).
- Request a free ACM (AWS Certificate Manager) certificate in
us-east-1(required for CloudFront regardless of your bucket's region). - Attach the certificate to the distribution and set your Alternate Domain Name (CNAME).
- Point your DNS (e.g., Route 53 Alias record) to the CloudFront distribution domain.
With this setup, your S3 bucket can remain private — CloudFront accesses it via an Origin Access Control (OAC), and all public traffic flows through the CDN over HTTPS.
Verification Checklist
- ✅ Visit the S3 website endpoint URL — you should see your
index.htmlrendered. - ✅ Navigate to a non-existent path — your
error.htmlshould appear (if configured). - ✅ Run
curl -I http://YOUR_BUCKET_NAME.s3-website-REGION.amazonaws.com— expectHTTP/1.1 200 OK. - ✅ Check browser DevTools Network tab — confirm assets (CSS, JS, images) load with correct
Content-Typeheaders.
Common Pitfalls
| Symptom | Root Cause | Fix |
|---|---|---|
403 Forbidden | Block Public Access still enabled, or bucket policy missing | Re-check Step 2 and Step 4 |
404 Not Found on root | Index document not set or index.html not at bucket root | Verify Static Website Hosting config and file path |
| CSS/JS not loading | Wrong Content-Type on uploaded assets | Re-upload with explicit --content-type |
| HTTPS not working | S3 website endpoint is HTTP-only by design | Add CloudFront + ACM certificate (Step 6) |
| Stale content after update | CloudFront edge cache serving old version | Create a CloudFront invalidation for /* |
Glossary
| Term | Definition |
|---|---|
| S3 Website Endpoint | A dedicated HTTP URL S3 exposes when Static Website Hosting is enabled; distinct from the S3 REST API endpoint and supports index/error document routing. |
| Block Public Access | An S3 safety mechanism (configurable at account and bucket level) that overrides bucket policies and ACLs to prevent unintended public exposure. |
| Bucket Policy | A resource-based IAM policy attached directly to an S3 bucket, controlling who can perform which actions on the bucket and its objects. |
| Origin Access Control (OAC) | A CloudFront mechanism that allows a distribution to securely fetch objects from a private S3 bucket without making the bucket publicly accessible. |
| ACM (AWS Certificate Manager) | AWS-managed service for provisioning free TLS/SSL certificates; certificates used with CloudFront must be provisioned in the us-east-1 region. |
Next Steps
Your static site is live. The natural progression is: S3 (HTTP) → CloudFront + ACM (HTTPS) → Route 53 (custom domain) → GitHub Actions (CI/CD sync on push). Refer to the official AWS documentation for each service: S3 Static Website Hosting | CloudFront Getting Started.
Related Posts
- 📄 S3 Access Denied Despite Public Object: How Block Public Access Overrides Object ACLs
- 📄 Route 53 Alias vs. CNAME Records: The Definitive Guide for Pointing Domains to an ALB
- 📄 CloudFront Cache Invalidation: Force-Refresh Stale Edge Content After S3 Updates
- 📄 DNS Failover with Route 53: Automatically Reroute Traffic to S3 When Your EC2 Goes Down
Comments
Post a Comment