Building griak.net: From Zero to Production
I wanted a personal infrastructure lab. A place to experiment, to learn, to build. Not a portfolio site with placeholder content—a real, running system that does things.
The requirements were simple but ambitious: - Public-facing web presence - Private ops dashboard for infrastructure monitoring - Email server with modern JMAP protocol - All accessible securely from anywhere - Built with TDD from day one
This is how it happened.
I chose technologies I wanted to learn deeper: - **Next.js 16** - App Router, Server Components, ISR - **Docker** - Containerization for reproducibility - **Tailscale** - Zero-trust networking with Funnel for public access - **Caddy** - Automatic HTTPS with Let's Encrypt - **Stalwart Mail** - Modern mail server with JMAP support - **Vitest** - Fast testing with native ESM support
griak-main: Ubuntu 24.04.4 LTS, 4 cores, 7.7GB RAM, sitting at 192.168.50.104 on my local network. Nothing fancy—just a box to build on.
\`\`\` services: web: Next.js app (port 3000 internal) caddy: Reverse proxy (ports 80, 443) mail: Stalwart (ports 25, 587, 993, 4190, 9999) \`\`\`
Simple. Three containers. Clean separation.
I'd built infrastructure before. Usually: write config, deploy, debug, repeat. This time I committed to TDD.
1. Write a failing test for what I wanted 2. Run it, watch it fail 3. Write the minimal code to make it pass 4. Refactor if needed
Example - Tailscale connectivity test:
\`\`\`typescript describe('Tailscale Connectivity', () => { it('should show tailscale as connected', () => { const status = execSync('tailscale status --json').toString(); const parsed = JSON.parse(status); expect(parsed.BackendState).toBe('Running'); }); }); \`\`\`
This test forced me to install and configure Tailscale before I could proceed. No assumptions. No "I'll set that up later."
59 tests later, everything worked. Not because I was careful—because the tests wouldn't let me proceed until it was right.
In the end:
| Component | Tests | |-----------|-------| | Tailscale connectivity | 4 | | Tailscale Funnel | 3 | | Auth.js configuration | 5 | | Middleware barrier | 5 | | JMAP client | 5 | | Ops dashboard | 6 | | Webhook endpoint | 5 | | ISR revalidation | 3 | | Time-Machine page | 4 | | Portfolio page | 3 | | Blog page | 3 | | Landing page | 4 | | Signin page | 4 | | Credential validation | 5 | | **Total** | **59** |
All passing. All tested. All working.
1. **TDD scales to infrastructure** - Tests aren't just for code. They're for anything you want to work reliably.
2. **SSH timeouts are real** - Long-running commands over SSH will die. Design for it.
3. **Tailscale Funnel is magic** - Secure external access without port forwarding is a game-changer.
4. **Docker networking has limits** - Bridge networks can't do everything. \`extra_hosts\` is your friend.
5. **Content matters** - A deployed site with placeholder content is just a demo. Real content makes it real.
---