245 lines
11 KiB
TypeScript
245 lines
11 KiB
TypeScript
'use client'
|
|||
|
|
|
||
|
|
import Link from 'next/link'
|
||
|
|
import { ArrowLeft } from 'lucide-react'
|
||
|
|
|
||
|
|
const skillCategories = [
|
||
|
|
{
|
||
|
|
title: 'Infrastructure & Orchestration',
|
||
|
|
skills: ['Kubernetes (Talos Linux)', 'ArgoCD (GitOps)', 'Terraform (IaC)', 'Kustomize'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Networking & Ingress',
|
||
|
|
skills: ['nginx Ingress Controller', 'Cloudflare Tunnel', 'CoreDNS', 'NetworkPolicy'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Storage',
|
||
|
|
skills: ['Longhorn (distributed block)', 'MinIO (S3-compatible)'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Databases',
|
||
|
|
skills: ['CloudNativePG (PostgreSQL)', 'pgvector (AI embeddings)'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Identity & Security',
|
||
|
|
skills: ['Authentik (OIDC SSO)', 'SOPS (encrypted secrets)', 'cert-manager (TLS)'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Observability',
|
||
|
|
skills: ['Prometheus', 'Grafana', 'Tempo (tracing)', 'OpenTelemetry', 'Loki (logs)'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'CI/CD',
|
||
|
|
skills: ['Forgejo (git + Actions)', 'Container Registry', 'DinD Runners'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'AI/ML Platform',
|
||
|
|
skills: ['vLLM (Qwen3-32B)', 'Ollama', 'TEI (embeddings)', 'KServe'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Workflow & Messaging',
|
||
|
|
skills: ['Temporal (durable workflows)', 'Kafka/Redpanda (streaming)'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Languages & Frameworks',
|
||
|
|
skills: ['Go (API gateway)', 'Python (ML)', 'Next.js (frontend)'],
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
const coreCompetencies = [
|
||
|
|
'Multi-tenant GPU scheduling (sm70/Volta constraints)',
|
||
|
|
'Zero-downtime GitOps deployments',
|
||
|
|
'Service mesh patterns without Istio overhead',
|
||
|
|
'Hybrid cloud networking (Cloudflare + bare-metal)',
|
||
|
|
'Declarative IAM with OIDC claim mapping',
|
||
|
|
]
|
||
|
|
|
||
|
|
export default function HomelabPage() {
|
||
|
|
return (
|
||
|
|
<main className="min-h-screen bg-white dark:bg-gray-950">
|
||
|
|
<div className="max-w-4xl mx-auto px-6 py-20">
|
||
|
|
<Link
|
||
|
|
href="/#project-homelab"
|
||
|
|
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-8"
|
||
|
|
>
|
||
|
|
<ArrowLeft size={16} />
|
||
|
|
Back to Projects
|
||
|
|
</Link>
|
||
|
|
|
||
|
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||
|
|
Homelab: Self-Hosted Cloud Platform
|
||
|
|
</h1>
|
||
|
|
<p className="text-xl text-gray-600 dark:text-gray-400 mb-12">
|
||
|
|
AWS rebuilt from scratch at home—full stack from compute to observability.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{/* Stats */}
|
||
|
|
<section className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 rounded-lg p-6 mb-12">
|
||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
||
|
|
<div>
|
||
|
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">4</div>
|
||
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Nodes</div>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">20+</div>
|
||
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Services</div>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">99.2%</div>
|
||
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Uptime</div>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">60%</div>
|
||
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">LLM Latency Cut</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Skills Grid */}
|
||
|
|
<section className="mb-12">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||
|
|
Technologies & Skills
|
||
|
|
</h2>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
{skillCategories.map((category) => (
|
||
|
|
<div
|
||
|
|
key={category.title}
|
||
|
|
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-800"
|
||
|
|
>
|
||
|
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||
|
|
{category.title}
|
||
|
|
</h3>
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{category.skills.map((skill) => (
|
||
|
|
<span
|
||
|
|
key={skill}
|
||
|
|
className="text-xs px-2 py-1 rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
|
||
|
|
>
|
||
|
|
{skill}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Core Competencies */}
|
||
|
|
<section className="mb-12">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||
|
|
Core Competencies Demonstrated
|
||
|
|
</h2>
|
||
|
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<ul className="space-y-2">
|
||
|
|
{coreCompetencies.map((item) => (
|
||
|
|
<li key={item} className="flex items-start gap-2 text-gray-700 dark:text-gray-300">
|
||
|
|
<span className="text-blue-500 mt-1">•</span>
|
||
|
|
{item}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Architecture Sections */}
|
||
|
|
<section className="mb-12">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||
|
|
Architecture Deep Dive
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
{/* Infrastructure */}
|
||
|
|
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||
|
|
Infrastructure Layer
|
||
|
|
</h3>
|
||
|
|
<p className="text-gray-700 dark:text-gray-300 mb-3">
|
||
|
|
4-node bare-metal cluster (3 control plane + 1 worker) running Talos Linux—immutable, API-driven OS designed for Kubernetes.
|
||
|
|
</p>
|
||
|
|
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||
|
|
<li>• <strong>Compute:</strong> Talos Linux nodes, machine config via Terraform</li>
|
||
|
|
<li>• <strong>Networking:</strong> Cilium CNI, nginx ingress, Cloudflare Tunnel for zero-trust external access</li>
|
||
|
|
<li>• <strong>Storage:</strong> Longhorn for distributed block storage with disk tagging, MinIO for S3</li>
|
||
|
|
<li>• <strong>GitOps:</strong> ArgoCD with multi-source Applications and sync waves</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Data Platform */}
|
||
|
|
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||
|
|
Data Platform
|
||
|
|
</h3>
|
||
|
|
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||
|
|
<li>• <strong>PostgreSQL:</strong> CloudNativePG operator with HA, automated failover</li>
|
||
|
|
<li>• <strong>Vector DB:</strong> pgvector extension for AI embeddings</li>
|
||
|
|
<li>• <strong>Streaming:</strong> Kafka/Redpanda for event-driven architecture</li>
|
||
|
|
<li>• <strong>Workflows:</strong> Temporal for durable, long-running processes</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* AI/ML */}
|
||
|
|
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||
|
|
AI/ML Platform
|
||
|
|
</h3>
|
||
|
|
<p className="text-gray-700 dark:text-gray-300 mb-3">
|
||
|
|
Self-hosted LLM inference with multi-tenant GPU scheduling and 60% latency reduction.
|
||
|
|
</p>
|
||
|
|
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||
|
|
<li>• <strong>Inference:</strong> vLLM serving Qwen3-32B with INT4 quantization</li>
|
||
|
|
<li>• <strong>Multi-model:</strong> Ollama for smaller models, hot-swapping</li>
|
||
|
|
<li>• <strong>Embeddings:</strong> TEI for text embeddings and reranking</li>
|
||
|
|
<li>• <strong>Orchestration:</strong> KServe + custom Go API gateway</li>
|
||
|
|
<li>• <strong>Scheduling:</strong> GPU node affinity with sm70/Volta constraints</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Security */}
|
||
|
|
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||
|
|
Identity & Security
|
||
|
|
</h3>
|
||
|
|
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||
|
|
<li>• <strong>SSO:</strong> Authentik OIDC provider with custom claims and group mapping</li>
|
||
|
|
<li>• <strong>RBAC:</strong> Kubernetes RBAC synced with Authentik groups</li>
|
||
|
|
<li>• <strong>Secrets:</strong> SOPS-encrypted secrets in git, decrypted at deploy time</li>
|
||
|
|
<li>• <strong>TLS:</strong> cert-manager with DNS-01 ACME challenges via Cloudflare</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Observability */}
|
||
|
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||
|
|
Observability Stack
|
||
|
|
</h3>
|
||
|
|
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||
|
|
<li>• <strong>Metrics:</strong> Prometheus with custom recording rules, Grafana dashboards</li>
|
||
|
|
<li>• <strong>Logs:</strong> Loki for aggregation, structured logging from all services</li>
|
||
|
|
<li>• <strong>Traces:</strong> Tempo + OpenTelemetry Collector for distributed tracing</li>
|
||
|
|
<li>• <strong>Alerts:</strong> AlertManager → Slack integration for incident response</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Applications */}
|
||
|
|
<section className="mb-12">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||
|
|
Self-Hosted Applications
|
||
|
|
</h2>
|
||
|
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{['Paperless-ngx', 'Immich', 'Homarr', 'Portainer', 'Forgejo', 'Authentik'].map((app) => (
|
||
|
|
<span
|
||
|
|
key={app}
|
||
|
|
className="text-sm px-3 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
||
|
|
>
|
||
|
|
{app}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
)
|
||
|
|
}
|