Self-Hosting with Claude

How I deployed a static site to AWS for under $1/month (minus Claude expense) — using Claude as the AI pair-programmer.

You can reference my setup here: github.com/allbytestudios/allbyte-web

1. The Setup

This site runs on a fully automated pipeline: I push code to GitHub, and it's live on allbyte.studio within 30 seconds. No servers to manage, no containers to orchestrate. Just static files on a CDN.

The entire infrastructure — AWS resources, CI/CD pipeline, DNS, certificates — was built in a single conversation with Claude Code. The same conversation that built the site also set up the hosting, debugged the deploy failures, and even generated this blog post documenting the process. That's the workflow: I describe what I want, Claude writes the code, I review and iterate, and we keep going.

My Tools

  • Claude Pro — AI pair-programmer via Claude Code (Anthropic's CLI). Writes the code, sets up infrastructure, debugs deploy issues, and built this document. Same setup I use for Godot game dev. I don't count the subscription as a hosting cost since I'm already paying for it for game development.
  • AWS account — S3, CloudFront, Route 53, ACM, CloudFormation, Lambda. Everything runs here.
  • My PC — Windows 11, Node.js, npm, AWS CLI, GitHub CLI. All development and deploys happen from one machine.

2. Technical Details

Architecture & Stack

GitHub (push to main)
       │
       ▼
GitHub Actions (OIDC auth)
       │
       ▼
  npm ci && npm run build
       │
       ▼
  aws s3 sync → S3 Bucket
       │
       ▼
  CloudFront CDN (HTTPS)
       │
       ▼
  allbyte.studio
  • Astro 6 — Static site generator with content collections for devlogs
  • Svelte 5 — Interactive UI (hover effects, audio, login modal)
  • Tailwind CSS v4 — Styling via Vite plugin
  • AWS S3 + CloudFront — Hosting and CDN
  • GitHub Actions + OIDC — CI/CD with no stored secrets
  • CloudFormation — Infrastructure as code (single template)

Costs & Protection

For a low-traffic site, the monthly bill looks like this:

ServiceMonthly Cost
S3 (static hosting)~$0.02
CloudFront CDN~$0.00–0.50
ACM Certificate (HTTPS)Free
Route 53 (DNS)$0.50/zone
GitHub Actions (CI/CD)Free
Cost protection (Budget + Lambda)Free
Total~$0.50–1.00/mo

CloudFront includes 1TB free transfer for the first year. After that, it's $0.085/GB — still negligible for a low-traffic site.

The scariest part of self-hosting on AWS isn't the setup — it's the fear of a surprise bill. If someone decides to DDoS the site or a bot hammers the CDN, CloudFront charges by the request. So I built an automatic kill switch.

A monthly AWS Budget watches my spend. At 80% ($20), I get an email warning. At 100% ($25), a Lambda function automatically disables the CloudFront distribution — the site goes down instead of racking up charges. I get an email notification either way.

CloudFront includes AWS Shield Standard for free, which handles common layer 3/4 DDoS attacks. For anything more sophisticated, you'd add AWS WAF (~$6/mo), but I figure for my low-traffic site the auto-shutoff is good enough.

To re-enable after a shutoff: flip the distribution back to Enabled in the CloudFront console. Total cost of the protection: $0 (Lambda free tier, SNS free tier, Budgets are free).

Infrastructure & Deploy

I registered allbyte.studio through Route 53, as suggested by Gemini. This allows DNS validation for the SSL cert to happen automatically — Claude added the CNAME records via the AWS CLI in seconds. Since Route 53 is already inside AWS, I can use native ALIAS records instead of CNAMEs, and everything — domain, DNS, certificate, CDN — lives under one account with one bill. Finally, if I'd used Squarespace, Namecheap, or GoDaddy, every DNS change would've been a context switch to a different UI with different terminology.

Everything lives in one CloudFormation template (infrastructure/cloudformation.yaml). A single command provisions:

  1. S3 Bucket — Private, no public access. CloudFront reads via Origin Access Control.
  2. CloudFront Distribution — HTTPS, HTTP/2+3, caching, error page routing.
  3. ACM Certificate — Free SSL for the domain + www. DNS-validated automatically.
  4. Response Headers Policy — Injects Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers globally. Required for Godot's SharedArrayBuffer.
  5. GitHub OIDC Provider + IAM Role — GitHub Actions authenticates via federation. No AWS access keys stored anywhere.
aws cloudformation deploy \
  --template-file infrastructure/cloudformation.yaml \
  --stack-name allbyte-studio-site \
  --capabilities CAPABILITY_NAMED_IAM \
  --region us-east-1

Traditional CI/CD stores AWS access keys as GitHub secrets. Claude recommended OIDC federation instead, and it's better in every way:

OIDCAccess Keys
Stored secretsNoneKey ID + Secret
Token lifetime~1 hourUntil rotated
Rotation neededNoYes
ScopeLocked to repo + branchWherever keys are used

The IAM role's trust policy restricts access to only my specific repository on the main branch. Even if someone forks the repo, they can't assume the role.

Every push to main triggers a GitHub Actions workflow. I push, and 30 seconds later it's live. The workflow:

  1. Checks out the code
  2. Installs dependencies (npm ci) with caching
  3. Builds the static site (npm run build)
  4. Authenticates to AWS via OIDC — no secrets needed
  5. Syncs the dist/ folder to S3
  6. Invalidates the CloudFront cache

Total deploy time: ~30 seconds from push to live.

Cache Strategy

Static assets (JS, CSS, images, fonts) get max-age=31536000, immutable — cached forever. Astro hashes filenames, so new deploys automatically get new URLs.

HTML files get max-age=0, must-revalidate — always revalidated so users see the latest content immediately after a deploy.

Asset Pipeline

The site pulls game assets directly from the Godot project on my local machine. A Node.js sync script (scripts/sync-assets.js) reads a config file (scripts/asset-manifest.json) and copies music, fonts, sprites, and backgrounds into the web project.

For music, it deduplicates across formats — Godot has both .ogg and .mp3 versions of tracks, so the script picks the preferred format and skips duplicates. Temporary placeholder music and Godot .import files are excluded automatically.

Sprites are the interesting part. The game stores character animations as horizontal sprite sheets — a single PNG with all frames side by side. A Python script (scripts/spritesheet-to-gif.py) splits these sheets into individual frames and assembles them into animated GIFs, scaled up with nearest-neighbor interpolation to keep the pixel art crisp. Each character's animations (idle, walk, attack, cast, etc.) become separate GIFs for the web.

The manifest is fully configurable — I pick which characters, which animations, the scale factor, and frame duration per character. Characters can be flagged as hidden for unreleased content. Everything outputs to public/assets/ with JSON indexes that the Astro pages consume at build time.

The generated files are committed to git since CI doesn't have access to the Godot project. Locally I run npm run sync to refresh, then push.

3. Lessons Learned

CloudFormation naming

Resource names can't contain dots. allbyte.studio-headers fails; allbyte-studio-headers works. Cost us a rollback.

Astro 6 requires Node.js 22+

First CI deploy failed because the workflow used Node 20. Check your framework's minimum version.

IAM permissions for S3 copy-in-place

Updating cache headers on already-uploaded files requires s3:GetObject in addition to PutObject. The copy operation reads then writes.

Tailwind v4 + Astro + Svelte CSS variables

CSS custom properties defined in a scoped Astro <style> block don't reach Svelte components. Use <style is:global> or hardcode values.

ACM certs must be in us-east-1

CloudFront only uses certificates from us-east-1, regardless of where your other resources are. Deploy the whole stack there.

CloudFront doesn't serve index.html for subdirectories

S3's DefaultRootObject only works for the root path /. A request to /self-hosting-with-claude/ returns a 403 because S3 doesn't know to look for index.html inside that prefix. The fix is a CloudFront Function that rewrites URIs — appending index.html to paths ending in / or paths without a file extension. Without this, every subpage silently falls back to the homepage via the error page config.