Static Hosting (S3/Cloudflare)
Static hosting serves pre-built HTML, CSS, and JavaScript files without server-side processing. It offers the best performance, scalability, and cost efficiency for content that does not require server-side rendering on every request.
Popular static hosting solutions include AWS S3 with CloudFront, Cloudflare Pages, Google Cloud Storage, and Azure Static Web Apps. This guide focuses on the two most common: S3 + CloudFront and Cloudflare Pages.
S3 provides durable, low-cost storage for static assets. CloudFront adds a global CDN with HTTPS, custom domains, and cache behavior controls.
| 1 | # Create S3 bucket for static hosting |
| 2 | aws s3 mb s3://my-app.example.com --region us-east-1 |
| 3 | |
| 4 | # Enable static website hosting |
| 5 | aws s3 website s3://my-app.example.com \ |
| 6 | --index-document index.html \ |
| 7 | --error-document index.html |
| 8 | |
| 9 | # Block public access (use OAI/CloudFront, not public bucket) |
| 10 | aws s3api put-public-access-block \ |
| 11 | --bucket my-app.example.com \ |
| 12 | --public-access-block-configuration \ |
| 13 | "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" |
| 14 | |
| 15 | # Create CloudFront Origin Access Identity (OAI) |
| 16 | aws cloudfront create-cloud-front-origin-access-identity \ |
| 17 | --cloud-front-origin-access-identity-config \ |
| 18 | "CallerReference=oai-my-app,Comment=OAI for my-app" |
| 19 | |
| 20 | # Deploy static files |
| 21 | npm run build |
| 22 | aws s3 sync dist/ s3://my-app.example.com --delete |
| 23 | |
| 24 | # Invalidate CloudFront cache after deploy |
| 25 | aws cloudfront create-invalidation \ |
| 26 | --distribution-id DISTRIBUTION_ID \ |
| 27 | --paths "/*" |
S3 bucket policy for CloudFront access:
| 1 | { |
| 2 | "Version": "2012-10-17", |
| 3 | "Statement": [ |
| 4 | { |
| 5 | "Effect": "Allow", |
| 6 | "Principal": { |
| 7 | "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1A2B3C4D5E6F7" |
| 8 | }, |
| 9 | "Action": "s3:GetObject", |
| 10 | "Resource": "arn:aws:s3:::my-app.example.com/*" |
| 11 | } |
| 12 | ] |
| 13 | } |
danger
Cloudflare Pages offers a simpler, integrated static hosting solution with automatic Git deployments, global CDN, and built-in preview URLs — similar to Vercel and Netlify but on Cloudflare's global network.
| 1 | # Install Wrangler CLI |
| 2 | npm install -g wrangler |
| 3 | |
| 4 | # Login to Cloudflare |
| 5 | wrangler login |
| 6 | |
| 7 | # Create a new Pages project |
| 8 | wrangler pages project create my-app \ |
| 9 | --production-branch main |
| 10 | |
| 11 | # Deploy to Pages |
| 12 | wrangler pages deploy dist/ \ |
| 13 | --project-name my-app \ |
| 14 | --branch main |
| 15 | |
| 16 | # Deploy preview (non-production) |
| 17 | wrangler pages deploy dist/ \ |
| 18 | --project-name my-app \ |
| 19 | --branch feat-new-ui |
| 20 | |
| 21 | # List deployments |
| 22 | wrangler pages deployment list --project-name my-app |
| 23 | |
| 24 | # Configure custom domain |
| 25 | wrangler pages domain set my-app example.com |
Cloudflare Pages configuration via _headers and _redirects files (same syntax as Netlify):
| 1 | # _headers — Security & cache headers |
| 2 | /* |
| 3 | X-Frame-Options: DENY |
| 4 | X-Content-Type-Options: nosniff |
| 5 | Referrer-Policy: strict-origin-when-cross-origin |
| 6 | Permissions-Policy: geolocation=() |
| 7 | Strict-Transport-Security: max-age=31536000; includeSubDomains |
| 8 | |
| 9 | /assets/* |
| 10 | Cache-Control: public, max-age=31536000, immutable |
| 11 | |
| 12 | # _redirects — Route configuration |
| 13 | /api/* https://api.example.com/:splat 200 |
| 14 | /blog/:slug /article/:slug 301 |
| 15 | /* /index.html 200 |
Proper caching is critical for static hosting performance. The goal is to cache immutable assets indefinitely (fingerprinted files) while ensuring HTML and API responses are always fresh.
| Asset Type | Cache TTL | Cache-Control |
|---|---|---|
| HTML pages | No cache | no-cache, must-revalidate |
| JavaScript (fingerprinted) | 1 year | public, max-age=31536000, immutable |
| CSS (fingerprinted) | 1 year | public, max-age=31536000, immutable |
| Images (processed) | 1 month | public, max-age=2592000, immutable |
| Fonts | 1 year | public, max-age=31536000, immutable |
| API responses | Varies | public, max-age=60, s-maxage=300 |
best practice
When you deploy new HTML files, you must invalidate the CDN cache to ensure users receive the latest version. Invalidation removes cached content from edge locations.
| 1 | # CloudFront: Create invalidation (costs per path) |
| 2 | aws cloudfront create-invalidation \ |
| 3 | --distribution-id E1A2B3C4D5E6F7 \ |
| 4 | --paths "/index.html" "/about/*" "/api/*" |
| 5 | |
| 6 | # Invalidate everything (use sparingly — costs more) |
| 7 | aws cloudfront create-invalidation \ |
| 8 | --distribution-id E1A2B3C4D5E6F7 \ |
| 9 | --paths "/*" |
| 10 | |
| 11 | # Cloudflare: Purge cache via API |
| 12 | curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ |
| 13 | -H "Authorization: Bearer $CF_API_TOKEN" \ |
| 14 | -H "Content-Type: application/json" \ |
| 15 | -d '{"files": ["https://example.com/", "https://example.com/about"]}' |
| 16 | |
| 17 | # Purge everything |
| 18 | curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ |
| 19 | -H "Authorization: Bearer $CF_API_TOKEN" \ |
| 20 | -H "Content-Type: application/json" \ |
| 21 | -d '{"purge_everything": true}' |
pro tip
Managing cloud resources manually is error-prone. Infrastructure as Code tools like Terraform and Pulumi define your infrastructure in version-controlled configuration files.
| 1 | # Terraform: S3 + CloudFront static hosting |
| 2 | provider "aws" { |
| 3 | region = "us-east-1" |
| 4 | } |
| 5 | |
| 6 | resource "aws_s3_bucket" "static" { |
| 7 | bucket = "my-app.example.com" |
| 8 | } |
| 9 | |
| 10 | resource "aws_s3_bucket_website_configuration" "static" { |
| 11 | bucket = aws_s3_bucket.static.id |
| 12 | |
| 13 | index_document { |
| 14 | suffix = "index.html" |
| 15 | } |
| 16 | |
| 17 | error_document { |
| 18 | key = "index.html" |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | resource "aws_cloudfront_distribution" "cdn" { |
| 23 | enabled = true |
| 24 | default_root_object = "index.html" |
| 25 | price_class = "PriceClass_100" # US + Europe only |
| 26 | |
| 27 | origin { |
| 28 | domain_name = aws_s3_bucket_website_configuration.static.website_endpoint |
| 29 | origin_id = "S3-${aws_s3_bucket.static.bucket}" |
| 30 | |
| 31 | custom_origin_config { |
| 32 | http_port = 80 |
| 33 | https_port = 443 |
| 34 | origin_protocol_policy = "http-only" |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | default_cache_behavior { |
| 39 | allowed_methods = ["GET", "HEAD"] |
| 40 | cached_methods = ["GET", "HEAD"] |
| 41 | target_origin_id = "S3-${aws_s3_bucket.static.bucket}" |
| 42 | |
| 43 | forwarded_values { |
| 44 | query_string = false |
| 45 | cookies { |
| 46 | forward = "none" |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | viewer_protocol_policy = "redirect-to-https" |
| 51 | min_ttl = 0 |
| 52 | default_ttl = 3600 |
| 53 | max_ttl = 86400 |
| 54 | } |
| 55 | |
| 56 | restrictions { |
| 57 | geo_restriction { |
| 58 | restriction_type = "none" |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | viewer_certificate { |
| 63 | cloudfront_default_certificate = true |
| 64 | } |
| 65 | } |
Static hosting is generally the most cost-effective deployment option. Here is a cost comparison for a typical site serving 100K monthly visits:
| Service | Monthly Cost | Includes |
|---|---|---|
| Cloudflare Pages (Free) | $0 | 500 builds/mo, unlimited bandwidth, 1GB storage |
| Cloudflare Pages (Pro) | $20 | Unlimited builds, early access, 50GB storage |
| S3 + CloudFront | ~$5-15 | S3 storage + requests + CloudFront data transfer |
| S3 + CloudFront + WAF | ~$25-40 | Adds WAF rules for DDoS/rate limiting protection |
info