I finally got around to building a proper personal site. Here’s how I did it and why I made the choices I did.
The Stack
- Astro 5 — Static site generator that ships zero JS by default
- Tailwind CSS — Utility-first CSS for rapid styling
- MDX — Markdown with components for blog posts
- Docker + nginx — Tiny container serving static files
- K3s + Flux — GitOps deployment on Hetzner Cloud
Why Astro?
For a personal site with a blog, I wanted something that outputs pure HTML and CSS. No client-side framework needed. Astro does exactly that — it builds to static files and only ships JavaScript when you explicitly ask for it.
The content collections API is clean:
const posts = await getCollection('blog');
And MDX support means I can embed components in blog posts when needed.
The Deploy Pipeline
The deployment is fully GitOps:
- Push to Gitea
- Gitea Actions builds the Docker image
- Image gets pushed to Gitea Container Registry
- Flux detects the new image and updates the deployment
The entire site runs in a single nginx container using about 5 MB of RAM. Hard to beat that.
Bento Grid Design
I went with a bento grid layout for the home page — asymmetric cards that look modern without being over-designed. Dark mode only, because it’s 2026 and my eyes are tired.
The cards use a simple fade-in animation on scroll:
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
Triggered by an IntersectionObserver — no heavy animation libraries needed.
What’s Next
- More blog posts about DevOps and infrastructure
- Project case studies with architecture diagrams
- Maybe a
/usespage for my setup
The source code is available on my Gitea instance.