How to Host a Static Website on AWS S3 (Step-by-Step Guide)
aws

How to Host a Static Website on AWS S3 (Step-by-Step Guide)

Learn how to host your static website on Amazon S3, configure permissions, and set up custom domains with CloudFront.

March 20, 2024
Admin
15 min read

Hosting Static Websites on AWS S3 with CloudFront: A Comprehensive Guide

TL;DR

This guide provides a detailed walkthrough for hosting static websites on AWS S3, leveraging CloudFront for global content delivery, HTTPS, custom domains, and performance optimization. It covers S3 bucket configuration, content uploading, CloudFront distribution setup, Route 53 DNS management, security best practices, cost considerations, and common troubleshooting scenarios. The recommended architecture prioritizes scalability, security, and cost-efficiency for modern static web applications.


Introduction

This document outlines the process of deploying and managing a static website using Amazon S3 for storage and CloudFront as a Content Delivery Network (CDN). This approach offers significant advantages in terms of availability, scalability, performance, and cost-effectiveness compared to traditional web hosting methods. We will cover the entire lifecycle, from initial setup to advanced configurations and ongoing maintenance.

Prerequisites

Essential AWS Services and Tools

| Requirement | Purpose | Notes | | :---------------- | :------------------------------------ | :------------------------------------------ | | AWS Account | Access to AWS services | Free tier eligibility applies for many services | | AWS CLI v2 | Command-line interface for AWS | Recommended for automation and scripting | | Domain Name | Custom URL for your website | Can be managed via Route 53 or external registrars | | Static Website | HTML, CSS, JavaScript, image files | Minification and optimization are key | | Index Document |

index.html
or equivalent | Required for S3 static website hosting | | Error Document |
error.html
or equivalent | For handling 404s and other errors |

Website Assets Preparation

| File Type | Required | Best Practice | | :------------- | :------- | :------------------------------------------------------ | | HTML | Yes | Minified, compressed (e.g., Gzip, Brotli) | | CSS | Yes | Bundled, minified, compressed | | JavaScript | Yes | Bundled, minified, compressed, consider code splitting | | Images | Yes | Optimized (e.g., WebP, JPEG), compressed | | Fonts | Optional | WOFF2 format for modern browsers, consider self-hosting |


1. S3 Bucket Setup for Static Website Hosting

This section details the creation and configuration of an S3 bucket to serve static website content.

Creating an S3 Bucket

AWS CLI:

aws s3api create-bucket \ --bucket your-unique-website-bucket-name \ --region us-east-1 \ --create-bucket-configuration LocationConstraint=us-east-1 # Omit for us-east-1

AWS Management Console: Navigate to S3, click "Create bucket," provide a globally unique name, select a region, and choose to block all public access (this will be reconfigured via bucket policy for website hosting).

Enabling Static Website Hosting

Configure the bucket to serve files as a website.

AWS CLI:

aws s3api put-bucket-website \ --bucket your-unique-website-bucket-name \ --website-configuration '{ "IndexDocument": {"Suffix": "index.html"}, "ErrorDocument": {"Key": "error.html"} }'

AWS Management Console: In the bucket's properties, find "Static website hosting," enable it, specify

index.html
as the Index document, and
error.html
as the Error document. Note the Endpoint URL provided by S3; this is crucial for initial testing but will be superseded by CloudFront.

Bucket Policy for Public Read Access

To allow unauthenticated access to your website files, apply a bucket policy.

AWS CLI:

aws s3api put-bucket-policy \ --bucket your-unique-website-bucket-name \ --policy '{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-unique-website-bucket-name/*" } ] }'

Important: This policy makes objects publicly readable. For production environments using CloudFront, it's recommended to restrict direct S3 access and use CloudFront Origin Access Identity (OAI) or Origin Access Control (OAC) for more secure access. This public policy is for simplicity in the initial S3 website endpoint testing phase.

CORS Configuration (If Required)

If your website needs to access resources from different domains (e.g., APIs), configure Cross-Origin Resource Sharing (CORS).

AWS CLI:

aws s3api put-bucket-cors \ --bucket your-unique-website-bucket-name \ --cors-configuration '{ "CORSRules": [ { "AllowedOrigins": ["http://localhost:3000", "https://your-domain.com"], "AllowedMethods": ["GET"], "MaxAgeSeconds": 3000, "AllowedHeaders": ["*"] } ] }'

Note: Adjust

AllowedOrigins
and
AllowedMethods
to your specific security requirements.


2. Uploading Website Content

Transfer your static assets to the configured S3 bucket.

File Organization Strategy

A typical project structure:

your-website-root/ ├── index.html ├── error.html ├── assets/ │ ├── css/ │ │ └── styles.min.css │ ├── js/ │ │ └── main.bundle.js │ └── images/ │ └── logo.webp ├── about/ │ └── index.html # Accessible at /about/ └── contact/ └── index.html # Accessible at /contact/

Key Principle: S3 treats objects as files. The directory structure in your upload maps directly to URL paths. For example,

about/index.html
becomes accessible at
your-bucket-url/about/
.

Uploading Files with AWS CLI

The

aws s3 sync
command is efficient for uploading. It synchronizes the local directory with the S3 bucket, uploading only new or modified files.

# Navigate to your website's root directory in the terminal cd your-website-root/ aws s3 sync . s3://your-unique-website-bucket-name \ --acl public-read \ # If using S3 website endpoint directly (not recommended with CloudFront OAC/OAI) --exclude "*.DS_Store" \ --exclude "node_modules/*" \ --cache-control "max-age=3600" \ # Default cache for most files --content-type "text/html" --exclude "*" --include "*.html" \ --content-type "text/css" --exclude "*" --include "*.css" \ --content-type "application/javascript" --exclude "*" --include "*.js" \ --content-type "image/webp" --exclude "*" --include "*.webp" \ --content-type "image/jpeg" --exclude "*" --include "*.jpg" --include "*.jpeg" \ --content-type "image/png" --exclude "*" --include "*.png"

Explanation of Flags:

  • .
    : Sync from the current local directory.
  • s3://your-unique-website-bucket-name
    : The target S3 bucket.
  • --acl public-read
    : Grants public read access to uploaded objects. Deprecated in favor of bucket policies and CloudFront OAC/OAI.
  • --exclude
    /
    --include
    : Fine-tune which files are uploaded.
  • --cache-control
    : Sets the
    Cache-Control
    HTTP header. Essential for performance.
  • --content-type
    : Explicitly sets the
    Content-Type
    header, crucial for browsers to render files correctly. Multiple
    --include
    and
    --content-type
    pairs are used to apply specific types.

Note on

public-read
ACL: While common for direct S3 website hosting, it's generally discouraged when using CloudFront. CloudFront's Origin Access Control (OAC) or Origin Access Identity (OAI) allows CloudFront to access S3 objects privately, enhancing security. The bucket policy should be adjusted to grant
GetObject
permissions to the OAI/OAC principal.

Content Type and Cache-Control Best Practices

| File Extension |

Content-Type
Header |
Cache-Control
Header | Notes | | :------------- | :---------------------------- | :------------------------------------ | :---------------------------------------- | |
.html
|
text/html
|
no-cache, max-age=0
| Revalidate frequently to ensure freshness | |
.css
|
text/css
|
public, max-age=604800, immutable
| Cache for 1 week,
immutable
for cache busting | |
.js
|
application/javascript
|
public, max-age=604800, immutable
| Cache for 1 week,
immutable
for cache busting | |
.jpg
,
.jpeg
|
image/jpeg
|
public, max-age=31536000, immutable
| Cache for 1 year | |
.png
|
image/png
|
public, max-age=31536000, immutable
| Cache for 1 year | |
.webp
|
image/webp
|
public, max-age=31536000, immutable
| Cache for 1 year | |
.svg
|
image/svg+xml
|
public, max-age=31536000, immutable
| Cache for 1 year | | Font files |
font/woff2
,
font/ttf
etc. |
public, max-age=31536000, immutable
| Cache for 1 year |

Cache Busting: For immutable assets (CSS, JS, images), append a version hash to the filename (e.g.,

styles.a1b2c3d4.css
). This ensures that when the file content changes, the URL changes, forcing browsers and CDNs to fetch the new version.


3. CloudFront Distribution Setup

CloudFront acts as the global front-end, caching content at edge locations and serving it to users with low latency.

Creating a CloudFront Distribution

  1. Origin Domain: Select your S3 bucket from the dropdown. Do NOT use the S3 website endpoint URL here if you plan to use OAC/OAI. CloudFront will automatically populate it.
  2. Origin Access:
    • Recommended: Choose "Origin access control settings (recommended)." Create a new OAC. This will generate a policy statement that you'll need to apply to your S3 bucket policy, granting CloudFront read access.
    • Legacy: Use "Legacy access identities" and create an OAI if necessary.
  3. Viewer Protocol Policy: Select "Redirect HTTP to HTTPS."
  4. Allowed HTTP Methods: Select "GET, HEAD, OPTIONS."
  5. Cache Policy: Use
    CachingOptimized
    for most static sites. You can create a custom policy for finer control.
  6. Price Class: "Use all edge locations" for maximum performance and availability globally.
  7. Alternate Domain Names (CNAMEs): Add your custom domain(s) (e.g.,
    example.com
    ,
    www.example.com
    ).
  8. Custom SSL Certificate: Select an ACM certificate for your custom domain (see Section 4).
  9. Default Root Object: Enter
    index.html
    . This ensures requests to
    /
    are served by
    index.html
    .
  10. Security Headers: Consider adding security headers via CloudFront Functions or Lambda@Edge (more on this in Advanced Configuration).

AWS CLI (Conceptual - requires

aws cloudfront create-distribution
with a JSON configuration file):

// distribution-config.json (simplified example) { "CallerReference": "your-unique-ref-id", "DistributionConfig": { "Origins": { "Quantity": 1, "Items": [ { "Id": "S3-your-unique-website-bucket-name", "DomainName": "your-unique-website-bucket-name.s3.amazonaws.com", // Use S3 API endpoint, not website endpoint "S3OriginConfig": { "OriginAccessIdentity": "origin-access-identity/cloudfront/E1111222333444" // Replace with your OAI if using legacy } } ] }, "DefaultCacheBehavior": { "TargetOriginId": "S3-your-unique-website-bucket-name", "ViewerProtocolPolicy": "redirect-to-https", "AllowedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"], "CachedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] } }, "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88632e7e9f" // Managed-CachingOptimized }, "Aliases": { "Quantity": 2, "Items": ["example.com", "www.example.com"] }, "DefaultRootObject": "index.html", "Enabled": true, "ViewerCertificate": { "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id", "SSLSupportMethod": "sni-only", "MinimumProtocolVersion": "TLSv1.2_2021" }, "HttpVersion": "http2" } }

After creating the distribution, CloudFront will provision it globally, which can take several minutes. Note the Distribution Domain Name (e.g.,

d123abc.cloudfront.net
).

S3 Bucket Policy Update for OAC/OAI

If you chose OAC or OAI, update your S3 bucket policy to grant CloudFront access:

Example OAC Policy:

{ "Version": "2012-10-17", "Statement": [ { "Sid": "GrantCloudFrontRead", "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-unique-website-bucket-name/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/YOUR_CLOUDFRONT_DISTRIBUTION_ID" } } } ] }

Replace

123456789012
with your AWS Account ID and
YOUR_CLOUDFRONT_DISTRIBUTION_ID
with your CloudFront distribution ID.


4. Custom Domain and HTTPS Configuration

This section covers mapping your custom domain to the CloudFront distribution and enabling HTTPS.

Requesting an SSL/TLS Certificate with AWS Certificate Manager (ACM)

  1. Region: Request the certificate in the
    us-east-1
    region for CloudFront.
  2. Domain Names: Enter your root domain (e.g.,
    example.com
    ) and any subdomains (e.g.,
    www.example.com
    ).
  3. Validation Method: Choose "DNS validation" for automated validation via Route 53.
  4. Create Records in Route 53: ACM will provide CNAME records. If your domain is managed by Route 53, ACM can often create these records for you automatically. Otherwise, manually add them to your DNS provider.

Once the certificate status is "Issued," it's ready to be used with CloudFront.

Configuring DNS with Route 53

If you are using Route 53 as your DNS provider:

  1. Hosted Zone: Ensure you have a hosted zone for your domain (e.g.,
    example.com
    ).
  2. Create Records:
    • For the root domain (
      example.com
      ):
      • Record Type:
        A
      • Alias: Yes
      • Route traffic to: Alias to CloudFront distribution.
      • Choose Distribution: Select your CloudFront distribution from the dropdown.
      • Routing Policy: Simple routing (or weighted/latency if applicable).
    • For the
      www
      subdomain (
      www.example.com
      ):
      • Record Type:
        A
      • Alias: Yes
      • Route traffic to: Alias to CloudFront distribution.
      • Choose Distribution: Select your CloudFront distribution.

If using an external DNS provider:

  • Root Domain: Use an
    ALIAS
    or
    ANAME
    record if supported by your provider, pointing to the CloudFront Distribution Domain Name. If not supported, you cannot easily point the root domain directly.
  • www
    Subdomain:
    Create a
    CNAME
    record for
    www
    pointing to your CloudFront Distribution Domain Name (e.g.,
    d123abc.cloudfront.net
    ).

Note on DNS Propagation: DNS changes can take time to propagate globally (from minutes to 48 hours).


5. Advanced Configuration and Security

Enhance your static website's security and functionality.

Implementing Security Headers

Security headers help mitigate common web vulnerabilities. These can be implemented using CloudFront Functions (preferred for performance and cost) or Lambda@Edge.

Example CloudFront Function (Viewer Request):

// viewer-response.js function handler(event) { const response = event.response; const headers = response.headers; // Add Security Headers headers['strict-transport-security'] = { value: 'max-age=31536000; includeSubdomains; preload' }; headers['content-security-policy'] = { value: "default-src 'self'; img-src 'self' data:; font-src 'self'; style-src 'self'; script-src 'self';" }; // Adjust directives as needed headers['x-content-type-options'] = { value: 'nosniff' }; headers['x-frame-options'] = { value: 'DENY' }; headers['x-xss-protection'] = { value: '1; mode=block' }; headers['referrer-policy'] = { value: 'strict-origin-when-cross-origin' }; return response; }

Associate this function with your CloudFront distribution's cache behavior for "Viewer Response" events.

Custom Error Responses

Configure CloudFront to serve custom error pages for specific HTTP status codes.

  1. In your CloudFront distribution settings, go to "Error Pages."
  2. Click "Create custom error response."
  3. HTTP Error Code: Select
    403
    (Forbidden) or
    404
    (Not Found).
  4. Customize Error Response: Yes.
  5. Response Page Path: Enter the path to your custom error file (e.g.,
    /error.html
    or
    /404.html
    ).
  6. HTTP Response Code: Keep the original code or set it to
    200
    if you want the error page to appear as a successful load (use with caution, can impact SEO and analytics). Usually, keep the original error code.

AWS WAF Integration

For advanced protection against common web exploits (SQL injection, cross-site scripting), integrate AWS WAF with your CloudFront distribution. Create WAF Web ACLs and associate them with your CloudFront distribution.


6. Performance Optimization

Maximize website speed and responsiveness.

CloudFront Cache Behaviors for Fine-Grained Control

Create multiple cache behaviors to apply different caching rules based on path patterns.

| Path Pattern | Origin Request Policy | Cache Policy | TTL (Min/Max/Default) | Compression | Notes | | :------------------ | :-------------------- | :------------------- | :-------------------- | :---------- | :--------------------------------------------------- | |

/assets/images/*
| AllViewer |
CachingOptimized
|
31536000/31536000/31536000
| No | Cache images for 1 year,
immutable
asset | |
/assets/css/*
| AllViewer |
CachingOptimized
|
604800/604800/604800
| Yes | Cache CSS for 1 week,
immutable
asset | |
/assets/js/*
| AllViewer |
CachingOptimized
|
604800/604800/604800
| Yes | Cache JS for 1 week,
immutable
asset | |
*.html
| AllViewer |
Managed-BypassCache
|
0/0/0
| Yes | Revalidate HTMLs to ensure freshness, compress them | |
*
| AllViewer |
Managed-BypassCache
|
0/0/0
| Yes | Fallback for any other file type, compress |

Origin Request Policy:

  • AllViewer
    : Forward all headers, cookies, and query strings.
  • CacheOptimized
    (managed policy): Only forwards necessary headers/cookies for caching. Use this unless specific dynamic behavior is needed.
  • Custom: Define precisely what to forward. For static sites, often minimal forwarding is required for cache hits.

Compression: Enable compression (Gzip and Brotli) within CloudFront cache policies for text-based assets (

.html
,
.css
,
.js
).

Image Optimization Strategies

  • Format Conversion: Use modern formats like WebP for smaller file sizes with comparable quality.
  • Compression: Employ tools like
    imagemin
    ,
    jpegoptim
    ,
    pngquant
    , or cloud-based image optimization services.
  • Responsive Images: Use the
    <picture>
    element or
    srcset
    attribute in HTML to serve appropriately sized images based on the viewport.

Lazy Loading

Implement lazy loading for images and other media that are below the fold to improve initial page load time.

// Example using Intersection Observer API document.addEventListener("DOMContentLoaded", function() { var lazyImages = document.querySelectorAll("img.lazy"); if ("IntersectionObserver" in window) { let lazyImageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { let lazyImage = entry.target; lazyImage.src = lazyImage.dataset.src; lazyImage.classList.remove("lazy"); lazyImageObserver.unobserve(lazyImage); } }); }); lazyImages.forEach(function(lazyImage) { lazyImageObserver.observe(lazyImage); }); } else { // Fallback for older browsers lazyImages.forEach(function(lazyImage) { lazyImage.src = lazyImage.dataset.src; lazyImage.classList.remove("lazy"); }); } });
<img data-src="path/to/your/image.jpg" src="placeholder.gif" alt="Description" class="lazy">

7. Monitoring, Analytics, and Logging

Understand your website's performance and usage.

CloudWatch Metrics for CloudFront

CloudFront automatically sends metrics to CloudWatch. Key metrics to monitor:

| Metric Name | Description | Threshold/Alerting | | :----------------- | :--------------------------------------------------- | :------------------------------------------------ | |

Requests
| Total number of viewer requests | Monitor trends, set alerts for sudden spikes | |
BytesDownloaded
| Total bytes transferred to viewers | Monitor for cost estimation, set alerts for anomalies | |
4xxErrorRate
| Percentage of requests resulting in 4xx errors | Set alert if > 1% | |
5xxErrorRate
| Percentage of requests resulting in 5xx errors | Set alert if > 0.1% | |
CacheHitRate
| Percentage of requests served from CloudFront cache | Aim for > 90% for static sites |

Access Logs

Enable CloudFront access logs to capture detailed information about each request. These logs are delivered to an S3 bucket.

  1. In your CloudFront distribution settings, under "Logging," enable logging.
  2. Specify an S3 bucket for log delivery.
  3. Choose a log prefix (e.g.,
    cloudfront-logs/
    ).

Analyzing these logs can provide insights into traffic sources, user agents, referrers, and more. Tools like Amazon Athena can query these logs directly from S3.

Website Analytics

Integrate a web analytics service (e.g., Google Analytics, Plausible, Matomo) by adding their tracking script to your

index.html
or a common layout file.


8. Cost Management

Optimize AWS costs for your static website.

Cost Optimization Strategies

  1. S3 Storage Class: Use S3 Standard for frequently accessed assets. Consider S3 Intelligent-Tiering if access patterns are unpredictable.
  2. S3 Lifecycle Policies: Implement policies to transition older, less accessed data to cheaper storage classes (e.g., S3 Glacier) or expire it.
  3. CloudFront Price Class: "Use all edge locations" provides the best performance but can be more expensive than regional options. Evaluate based on your target audience's geographic
AWS
S3
Static Website
CloudFront
Hosting