develicit
← Back to posts

Why I self-host my blog on AWS

·5 min read

When building a blog, the first question is where to host it. Most Next.js blogs end up on Vercel — one git push deploys it and preview environments come for free. It's frictionless.

But this blog compiles to fully static files at build time (output: 'export'). No server-side rendering, no edge functions. So is there a reason to pay for a PaaS abstraction I don't use? For a static site, running it yourself on AWS S3 + CloudFront is

  • cheap — under $1/mo for a small blog
  • fast — cache-served from CloudFront's global edge
  • less lock-in — drops the Vercel dependency for direct control over Lambda@Edge, security headers, and cache policy (though it does add AWS dependencies: CloudFront, OAC, Route 53)

This post is the record of that decision — and a direct, modeled answer to the common worry: "won't AWS end up costing more once traffic grows?"

The architecture at a glance

First, how the pieces fit together.

ComponentRole
S3Static asset storage, public access blocked, OAC-only
CloudFrontHTTPS, compression, security headers, global CDN
ACMTLS cert (must live in us-east-1 for CloudFront)
Route 53DNS, apex (root domain) alias
GitHub ActionsKeyless deploys via OIDC

A visitor request flows DNS → CloudFront edge → OAC-signed → private S3. Only dynamic bits like view counts are split off to a separate Cloudflare Worker. (Workers/KV free limits are 100k req/day and 1k KV writes/day — fine for a small blog, but if you POST a view count per PV, watch the limits/cost at high traffic.)

Client
Visitor browser
requests develicit.com
DNS
Route 53
hosted zone · apex A/AAAA alias → CloudFront
Edge / CDN
CloudFront distribution
global edge cache
ACM TLS cert (us-east-1)HTTP/2 · HTTP/3Brotli · Gzip compressionSecurity-headers policy (HSTS, …)SPA 404 → index mapping
Edge / CDN
OAC (SigV4 signed)
only this distribution can read S3 — enforced by bucket policy
Origin (private)
S3 bucket
Block Public Access · next-export (out/) static assets
View counter (sidecar)
Cloudflare Worker
POST /views/:slug
Workers KV
persists per-slug counts

On a cache hit, assets serve from the AWS edge (cache misses / HTML revalidation reach the S3 origin); only the dynamic view count lives on a Cloudflare Worker + KV — the S3 origin is never exposed directly.

Fig 1. Request-path architecture — DNS → edge (CloudFront) → OAC-signed → private S3. View counts split off to a Cloudflare Worker.

Three decisions are central to the design:

  • S3 is never made public. Block Public Access stays on, and the bucket policy only allows requests signed by CloudFront's OAC (Origin Access Control) via SigV4. Even if the bucket URL leaks, it can't be hit directly.
  • The TLS cert (ACM) lives in us-east-1. CloudFront is a global service that only reads certs from N. Virginia. The site itself can live in any region.
  • The apex domain uses an alias record. A root domain like develicit.com can't use a CNAME, so a Route 53 alias (A/AAAA) points straight at the CloudFront distribution.

Deploy: one git push

Static infra doesn't mean manual deploys. A push to main makes GitHub Actions mint short-lived credentials via OIDC, then build → sync to S3 → invalidate CloudFront in one shot.

1git push → main

Triggers the GitHub Actions deploy workflow

2OIDC auth

GitHub OIDC token → STS AssumeRoleWithWebIdentity → short-lived creds

keyless (no secrets)
3Build

pnpm install --frozen-lockfile → next export (out/) + Pagefind index

4S3 sync

_next/ immutable (1-yr cache), the rest max-age=0 + --delete

two-tier cache policy
5CloudFront invalidate

create-invalidation --paths "/*" refreshes the edge cache

No long-lived access keys live in GitHub — OIDC mints short-lived STS credentials per run to assume the deploy role.

Fig 2. Deploy pipeline — one git push cascades into keyless OIDC auth → build → S3 sync → cache invalidation, all automatically.

Two things matter here.

  • Keyless deploys. No long-lived AWS access keys sit in GitHub Secrets. Instead, GitHub's OIDC token calls AssumeRoleWithWebIdentity to obtain expiring, per-run credentials. The leak risk simply disappears.
  • Two-tier cache policy. Hash-stamped _next/ assets are cached immutable for a year; HTML and the rest use max-age=0 so they revalidate every time. New posts show up instantly while static assets live as long as possible at the edge.

Cost analysis

Fixed monthly cost (low traffic)

For a small personal blog (~10k PV/month ≈ ~5 GB transfer), CloudFront transfer/requests and S3 requests mostly fall inside the always-free tier (CloudFront's 1 TB transfer + 10M requests per month). So the real fixed cost collapses to essentially one line item.

  • CloudFront: ~$0 (within free tier)
  • S3: ~$0.05 (storage + a few requests)
  • Route 53: $0.50 (hosted zone, fixed regardless of traffic)
  • ACM: free

About $0.5–1/mo — basically just the Route 53 hosted zone. Against Vercel Pro ($20), that's roughly a 20× gap. (For a personal/non-commercial blog, though, Vercel Hobby is free and includes 100 GB/mo transfer, so the $20 comparison only applies when you actually need Pro.)

AWS self-hosting~$0.7/mo
Route 53 (hosted zone) $0.5S3 $0.1CloudFront (in free tier) $0.1
Vercel Pro$20/mo

About an 18× gap for a low-traffic personal blog. What happens as traffic grows is worked out concretely in Fig 4 below.

Fig 3. Monthly cost comparison (at ~10k pageviews) — transfer/requests sit in the free tier, so the Route 53 hosted zone is nearly the entire fixed cost.

Does the break-even flip as traffic grows?

People assume "if traffic spikes, AWS eventually costs more," but for a static site there's effectively no dollar break-even. Two reasons:

  • Both include 1 TB/mo egress free. CloudFront is free up to 1 TB/month; Vercel Pro includes 1 TB too. At 0.5 MB average transfer per pageview, 1 TB is about 2M pageviews/month. Up to that point AWS stays essentially flat ($1) while Vercel is flat at $20.
  • AWS's overage rate is also lower. Past the free tier, CloudFront is $0.085/GB (US/EU) vs Vercel's ~$0.15/GB. So even at high traffic the marginal rate keeps AWS ahead — it never flips.

Free-tier regime — up to ~2M PV/month

Both CloudFront and Vercel include 1 TB egress. Here AWS is essentially flat no matter the traffic.

10k PV~5 GB
AWS self-hosting
$0.7/mo
Vercel Pro
$20/mo
1M PV~0.5 TB
AWS self-hosting
$0.7/mo
Vercel Pro
$20/mo
2M PV~1 TB (cap)
AWS self-hosting
$0.7/mo
Vercel Pro
$20/mo

Beyond the free tier — egress rate comparison

AWS's overage rate is lower ($0.085/GB < $0.15/GB) → it doesn't flip even at high traffic.

5M PV~2.5 TB
AWS self-hosting
$130/mo
Vercel Pro
$250/mo
10M PV~5 TB
AWS self-hosting
$350/mo
Vercel Pro
$630/mo

For a static site, AWS is cheaper across the board. The $20/mo doesn't buy a lower bill — it buys DX: preview deploys, build minutes, image optimization.

AWS does get expensive fast in two cases — (1) costly edge regions like India/South America/some of APAC (~$0.11–0.12/GB vs $0.085 for US/EU), (2) multi-MB page payloads that spike per-PV transfer. Lowering per-PV egress via caching and image optimization IS cost control.

Assumptions: 0.5 MB average egress per PV, CloudFront US/EU $0.085/GB (1 TB/mo free), Vercel Pro 1 TB included then $0.15/GB, Route 53 $0.5 fixed. Only transfer is modeled; at very high PV, request charges add up (CloudFront beyond 10M requests, Vercel Edge Requests $0.002/1K). Real cost varies with page weight, region, and cache hit ratio.

Fig 4. Cost vs traffic — both platforms include ~1 TB/mo egress free, so the break-even is about operational effort, not dollars.

So for a static blog, $20/mo doesn't buy a lower bill — it buys DX: preview deploys, build minutes, image optimization. The real break-even is "how much of your own time will you spend." The two exceptions where AWS gets expensive fast: (1) costly edge regions like India/South America, and (2) multi-MB page payloads that spike per-PV transfer — so keeping per-PV egress low via caching and image optimization is cost control.

Trade-offs

There's no free lunch, of course. With self-hosting you pay the cost in effort, not dollars.

  • More upfront setup. Instead of wiring S3 · CloudFront · OAC · ACM · Route 53 by hand, I codified it in Terraform — but that's a learning curve of its own.
  • Preview environments are DIY. There's no per-PR preview URL like Vercel's out of the box; you build your own bucket/distribution if you want one.
  • Image optimization is on you. Instead of next/image's runtime optimization, you do it at build time or bolt on CloudFront + Lambda@Edge yourself.

In return, once it's built, maintenance is near zero. The infra is frozen as Terraform code, so reproducibility is 100% — and I can see exactly how every part works.

Wrapping up

To sum up: when you have a static site · need Vercel Pro (the free Hobby tier isn't enough) · mostly US/EU traffic · and the willingness to operate it yourself, AWS self-hosting wins on cost and speed. Conversely, if the free Hobby tier is enough, or you need SSR/ISR, don't want to spend time on infra, or rely on team-wide preview collaboration, Vercel is a perfectly reasonable call.

I'm in the camp of "I want to understand, line by line, how my blog is served" — so I picked AWS. That the choice never leaves me regretting it on the monthly bill is exactly why I wrote this up.


Comments