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

StepActionKey Detail
1Create S3 BucketName must match your domain if using Route 53; choose your target region
2Disable Block Public AccessAccount-level AND bucket-level settings must both allow public access
3Enable Static Website HostingSet index document (index.html) and optional error document
4Attach a Bucket PolicyGrant s3:GetObject to Principal: "*" for public reads
5Upload Your FilesMaintain relative paths; set correct Content-Type metadata
6(Optional) Add CloudFrontHTTPS, 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.

graph LR Browser["🌐 Browser"] CF["☁️ CloudFront
(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
  1. Browser issues an HTTP GET to the S3 website endpoint URL.
  2. S3 Website Endpoint (distinct from the REST API endpoint) handles the request — it understands index documents and error routing.
  3. S3 evaluates the Bucket Policy to confirm s3:GetObject is permitted for the anonymous principal.
  4. The matching object (e.g., index.html) is returned with its stored Content-Type.
  5. 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.

graph LR User["🌐 User Browser"] DNS["Route 53
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
  1. 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).
  2. Request a free ACM (AWS Certificate Manager) certificate in us-east-1 (required for CloudFront regardless of your bucket's region).
  3. Attach the certificate to the distribution and set your Alternate Domain Name (CNAME).
  4. 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.html rendered.
  • ✅ Navigate to a non-existent path — your error.html should appear (if configured).
  • ✅ Run curl -I http://YOUR_BUCKET_NAME.s3-website-REGION.amazonaws.com — expect HTTP/1.1 200 OK.
  • ✅ Check browser DevTools Network tab — confirm assets (CSS, JS, images) load with correct Content-Type headers.

Common Pitfalls

SymptomRoot CauseFix
403 ForbiddenBlock Public Access still enabled, or bucket policy missingRe-check Step 2 and Step 4
404 Not Found on rootIndex document not set or index.html not at bucket rootVerify Static Website Hosting config and file path
CSS/JS not loadingWrong Content-Type on uploaded assetsRe-upload with explicit --content-type
HTTPS not workingS3 website endpoint is HTTP-only by designAdd CloudFront + ACM certificate (Step 6)
Stale content after updateCloudFront edge cache serving old versionCreate a CloudFront invalidation for /*

Glossary

TermDefinition
S3 Website EndpointA 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 AccessAn S3 safety mechanism (configurable at account and bucket level) that overrides bucket policies and ACLs to prevent unintended public exposure.
Bucket PolicyA 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

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

EC2 SSH Connection Timeout: The Exact Security Group Rules You Need to Fix It

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User