Ten hours from start to production. A site that's fast to load, fast to update, and fast to extend. Here's how I got there, and what it uses.
The Stack in Practice:
Frontend: Astro (SSR)
API: Hono (Node.js)
CMS: Directus
Database: PostgreSQL
Infrastructure: Docker Compose
Deployment: Cloudflare Tunnels
Not arbitrary. Each choice paid dividends.
What Astro Actually Delivered
Shipped: 12KB JavaScript (total)
v0.dev's Next.js template: ~200KB JavaScript minimum. Just for the framework.
Astro conversion: 12KB total. Most pages are pure HTML.
What this means:
- First Contentful Paint: <0.8s
- Time to Interactive: <1.2s
- Lighthouse score: 97
The trade: Lost React interactivity. Gained speed.
Was it worth it? For a portfolio? Absolutely. I don't need client-side state management for showing projects.
Build Time: 8 Seconds
Next.js dev server startup: 15-20 seconds. Every change: 2-3 seconds hot reload.
Astro: 8 second full build. Changes: instant.
What this means during development:
- Change a component → See it immediately
- No waiting for webpack rebuilds
- Tight feedback loop = faster iteration
The multiplier: 20 small changes in 10 minutes vs 20 small changes in 30 minutes.
Content Routes: Zero Configuration
Needed routes for /blog/[slug] and /work/[slug].
Next.js approach:
- Set up dynamic routes
- Configure getStaticPaths
- Handle ISR if you want updates
- Deploy to regenerate
Astro approach:
// pages/blog/[slug].astro
const { slug } = Astro.params
const post = await getBlogPost(slug)
That's it. SSR by default. No build required for content updates.
What this delivered: Update a post in Directus → Refresh → See it. No deployment.
What Directus Actually Delivered
Non-Developer Content Updates
My partner can now:
- Add a new work project (no code)
- Update project descriptions (no code)
- Reorder featured projects (drag and drop)
- Fix typos in my bio (instant)
Before (hardcoded content): Every change = text me → I edit code → commit → deploy → 10 minutes.
Now: Every change = edit in Directus UI → save → instant.
The unlock: I'm not the bottleneck anymore.
Real Relational Data
Work projects have:
- Technologies (many-to-many)
- Categories (employment, hackathon, research)
- Status (published, draft, archived)
Strapi/Payload: Would work, but Directus's relational UI is better.
What this means: Filtering "show me all hackathon projects using TypeScript" is a UI dropdown, not a query I need to write.
TypeScript SDK Out of the Box
import { createDirectus, rest, readItems } from '@directus/sdk'
const directus = createDirectus<Schema>('http://directus:8055').with(rest())
// Fully typed
const projects = await directus.request(
readItems('work_projects', {
filter: { status: { _eq: 'published' } },
sort: ['order']
})
)
What this delivered:
- Autocomplete for all fields
- Type errors if I misspell a field name
- Build fails before runtime errors
The save: Caught 8 bugs during development that would have been runtime errors in production.
What Hono Actually Delivered
200KB → 12KB API Layer
Express is 200KB+. Hono is 12KB.
Does it matter? For cold starts: yes.
Reality check: This API runs in Docker on my homelab. Cold starts aren't an issue. I chose Hono for DX, not bundle size.
Type-Safe Routing
// api/src/index.ts
app.get('/api/work-projects/:slug', async (c) => {
const slug = c.req.param('slug') // TypeScript knows this is a string
const project = await getWorkProject(slug)
return c.json({ data: project }) // TypeScript validates the response
})
Express equivalent: Type safety is manual. You add types yourself or use separate libraries.
What this delivered: Zero "undefined is not a function" errors. The API layer just works.
Edge-Ready (Future Proof)
I'm running this on Docker now. If I ever move to Cloudflare Workers or Deno Deploy, Hono already runs there.
Express: Tied to Node.js.
Hono: Platform-agnostic.
The bet: Web runtimes are converging. Hono works everywhere.
What Docker Compose Actually Delivered
Identical Dev/Prod
My laptop: docker compose up
My homelab server: docker compose up
Same configs. Same behavior. No surprises.
What this prevented:
- "Works on my machine" → It works everywhere
- Environment variable mismatches → All in
.env - Service startup races → Healthchecks enforce order
The save: Zero deployment debugging. If it works locally, it works in production.
One Command Setup
New machine? Clone repo. Run:
docker compose up
That's it. Frontend, API, Directus, PostgreSQL all running. Connected. Networked. Ready.
Without Docker: Install Node. Install PostgreSQL. Configure PostgreSQL. Set up networking. Configure environment variables. Debug why they can't talk to each other.
With Docker: One command.
Time saved: 2 hours of setup → 2 minutes.
What Cloudflare Tunnels Actually Delivered
Zero Network Configuration
Traditional self-hosting requirements:
- Port forwarding on router
- Dynamic DNS for changing IP
- SSL certificate management
- Firewall rules
- DDoS protection (hope for the best)
Cloudflare Tunnel:
cloudflared tunnel --url http://localhost:4321
That's it. HTTPS included. DDoS protection included. No exposed ports.
What this means: I can self-host on residential internet without telling my ISP or configuring my router.
Free HTTPS
Let's Encrypt works, but you need to:
- Run certbot
- Configure renewal
- Handle challenges
- Set up nginx/caddy
- Debug when it breaks
Cloudflare Tunnel: HTTPS by default. Zero configuration.
Time saved: 4 hours of SSL debugging → 0 minutes.
The Performance Reality
Lighthouse Scores
- Performance: 97
- Accessibility: 100
- Best Practices: 100
- SEO: 100
What got us here:
- Astro's minimal JavaScript
- Optimized image loading
- Server-side rendering
- Clean HTML structure
The baseline: v0.dev template scored 85-90. Conversion to Astro added 10 points.
Real User Metrics
First Contentful Paint: 0.6-0.8s Time to Interactive: 1.0-1.2s Largest Contentful Paint: 1.1s
For comparison:
- Average Next.js site: 2-3s FCP
- Average portfolio site: 3-4s FCP
Why: No React hydration. No JavaScript bundles. Pure HTML until needed.
Backend Response Times
API response times (p95):
/api/posts: 45ms/api/work-projects: 38ms/api/profile: 22ms
Why fast:
- Hono is lightweight
- PostgreSQL is local (same Docker network)
- No ORM overhead (direct Directus SDK)
The Developer Experience
Hot Reload: Actually Hot
Change a component → See it in <100ms
Why:
- Astro's fast refresh
- Docker volume mounts (no rebuild needed)
- Minimal build pipeline
The difference: Tight feedback loop. Iterate 10x in an hour, not a day.
Type Safety Across Boundaries
// Directus schema
interface WorkProject { title: string }
// API layer mirrors it
interface WorkProject { title: string }
// Frontend mirrors it
interface WorkProject { title: string }
Misspell title anywhere → Build fails.
What this prevented: 5-6 bugs that would have been "why is this undefined?" in production.
Error Messages That Actually Help
Docker error:
network homelab-network declared as external, but could not be found
Fix: docker network create homelab-network
Directus error:
Field "slug" is required but not provided
Fix: Add slug to the request.
Why this matters: Debugging is fast when errors are clear.
What This Enables
Content Updates Without Deployment
Before: Edit markdown → Commit → Push → Build → Deploy → 5 minutes
Now: Edit in Directus → Save → Instant
Real example: Fixed a typo in my bio. Took 10 seconds. No deployment.
Easy to Extend
Want to add a "talks" section?
- Create
talkscollection in Directus (5 minutes) - Add API endpoint in Hono (2 lines)
- Create Astro page (copy/paste work page pattern)
- Done
Time: 15 minutes. No refactoring. No breaking changes.
Self-Hosting Without Pain
This runs in my closet on a Debian server.
Costs:
- Hardware: $200 one-time (mini PC)
- Electricity: ~$3/month
- Cloudflare Tunnel: Free
vs Vercel/Netlify:
- Free tier limits: 100GB bandwidth, then $20/100GB
- Pro tier: $20/month minimum
Break-even: After 3 months, self-hosting is cheaper.
The Honest Trade-offs
What I Lost
React Interactivity: Can't build complex SPAs. Fine for a portfolio.
Vercel Niceties: No preview deploys. No automatic HTTPS per branch. But I didn't need them.
Managed Database: I maintain PostgreSQL myself. But Docker makes it trivial.
What I Gained
Speed: 97 Lighthouse score. <1s interactive.
Control: My data, my server, my rules.
Cost: $3/month electricity vs $20/month hosting.
Flexibility: Can run anything on this server, not just web apps.
The Results
Live: https://b28.dev
Performance:
- Lighthouse: 97
- FCP: <0.8s
- TTI: <1.2s
DX:
- Hot reload: <100ms
- Type-safe: Build fails on errors
- One-command setup:
docker compose up
Maintainability:
- Content updates: 10 seconds
- Add new sections: 15 minutes
- Deploy changes:
git pull && docker compose up -d
Cost:
- Development: 10 hours active work
- Hosting: $3/month electricity
- Maintenance: ~1 hour/month
The Takeaway
These weren't random choices. Each tool delivered specific value:
- Astro: Speed (12KB JS, 97 Lighthouse)
- Directus: Non-developer updates
- Hono: Type-safe API, edge-ready
- Docker: Identical dev/prod
- Cloudflare Tunnels: Zero-config HTTPS
The pattern: Choose tools for what they deliver, not what's popular.
Template credit: Felix Macaspac's v0.dev minimalist portfolio
The velocity shift: Right tools → Fast site, fast updates, fast iteration