Go HTTP/3 server with a custom typewriter markup parser that streams content character-by-character to browsers and renders ANSI-styled text for terminals (curl https://rayhanadev.com).
Requires Go 1.25+ and TLS certificates (defaults to configs/certs/{cert,key}.pem, override with TLS_CERT_PATH / TLS_KEY_PATH env vars). Listen address defaults to :3000 (override with LISTEN_ADDR).
task dev # run dev server on :3000
task build # compile to bin/site
task lint # golangci-lint
task format # gofmt
task generate:wrangler # regenerate wrangler.jsonc from static assets
task deploy:static # generate wrangler config + deploy to Cloudflare Workers
task deploy:server:build # build and push Docker image to GHCR
task deploy:server # SSH into production and pull latest imageTwo path-filtered GitHub Actions workflows run on pushes to main:
Deploy Server — triggered by changes to Go source, Dockerfile, or docker-compose.yml (excludes internal/assets/static/):
- Build image — builds the Docker image and pushes it to
ghcr.io/rayhanadev/site - Deploy server (after build) — SSHs into the GCP instance via Tailscale, pulls the new image, and restarts via Docker Compose
Deploy Static Assets — triggered by changes to internal/assets/static/ or the wrangler config generator:
- Deploy static assets — generates
wrangler.jsoncfrom the static assets directory and deploys to Cloudflare Workers via Wrangler
| Secret | Description |
|---|---|
TS_OAUTH_CLIENT_ID |
Tailscale OAuth client ID |
TS_OAUTH_SECRET |
Tailscale OAuth client secret |
CLOUDFLARE_API_TOKEN |
Cloudflare API token for Wrangler |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account ID |
All infrastructure is codified in terraform/. Managed resources:
- GCP — VPC, subnet, static IP, firewall (Cloudflare-only origin access), e2-micro instance with cloud-init
- Cloudflare — DNS records, SSL Full (Strict), Origin CA certificate, www-to-apex redirect
- Tailscale — auth key, ACL policy (SSH access for admin and CI)
- Terraform or OpenTofu >= 1.7
- GCP project with billing enabled
- Cloudflare API token (Zone:Edit, DNS:Edit, Origin CA permissions)
- Tailscale OAuth client credentials
Create the GCS bucket for remote state:
cd terraform/bootstrap
cp terraform.tfvars.example terraform.tfvars # fill in gcp_project_id
tofu init
tofu applycd terraform
cp terraform.tfvars.example terraform.tfvars # fill in all values
# Update the backend bucket name in terraform.tf to match your project ID,
# or pass it at init time:
tofu init -backend-config="bucket=YOUR_PROJECT_ID-terraform-state"
tofu plan # review changes
tofu apply # provision everything