+ Getting Started
+ Welcome! Follow these steps...
+ ...
+
+```
+
+**Advantages**:
+- Zero new dependencies
+- Works with any Svelte 5 version
+- Content is still just HTML/text in clearly named files
+- Can add Svelte interactivity later (copy buttons, progress tracking)
+
+**Trade-off**: Content edits require touching `.svelte` files instead of `.md`. For 5 pages maintained by one person (or an AI), this is fine. If content grows significantly, revisit mdsvex later when Svelte 5 compatibility is stable.
+
+### Shared Content Styling
+Create `src/lib/content.css` with the docs-style layout:
+```css
+.content { max-width: 768px; margin: 2rem auto; font-family: system-ui; line-height: 1.6; }
+.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
+.content pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; }
+.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
+.content .callout { background: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin: 1rem 0; }
+.content .danger { background: #f8d7da; border-left: 4px solid #dc3545; }
+```
+
+---
+
+## Part 3: Route Structure
+
+```
+src/routes/
+├── +layout.svelte ← Nav bar (Home, Onboarding, Architecture, Services, Contributing, Troubleshooting)
+├── +page.svelte ← Identity + VPN callout + Get Started (UPDATED)
+├── onboarding/+page.svelte ← Step-by-step guide
+├── architecture/+page.svelte ← How the cluster works
+├── services/+page.svelte ← Service catalog
+├── contributing/+page.svelte ← PR workflow
+├── troubleshooting/+page.svelte ← Common issues
+├── setup/+page.svelte ← Existing kubectl install
+├── setup/script/+server.ts ← Existing auto-setup (FIXED)
+└── download/+server.ts ← Existing kubeconfig download (FIXED)
+```
+
+### Navigation Layout (`+layout.svelte`)
+Simple horizontal nav, active page highlighted:
+```svelte
+
+ Architecture
+
+
+ Overview
+ The infrastructure runs on a single Dell R730 server (22 CPU cores, 142GB RAM) using Proxmox to manage virtual machines. Five of those VMs form a Kubernetes cluster that runs 70+ services.
+
+Proxmox (Dell R730)
+ ├── k8s-master (10.0.20.100) — control plane
+ ├── k8s-node1 (10.0.20.101) — GPU node (Tesla T4)
+ ├── k8s-node2 (10.0.20.102) — worker
+ ├── k8s-node3 (10.0.20.103) — worker
+ ├── k8s-node4 (10.0.20.104) — worker
+ ├── TrueNAS (10.0.10.15) — storage (NFS + iSCSI)
+ └── pfSense (10.0.20.1) — firewall + gateway
+
+
+
+ Networking
+
+ - Public domain:
viktorbarzin.me — managed by Cloudflare
+ - Internal domain:
viktorbarzin.lan — managed by Technitium DNS
+ - Ingress: Cloudflare → Traefik → services
+ - VPN: Headscale (self-hosted Tailscale)
+
+
+
+
+ Storage
+
+ - NFS (
nfs-truenas) — for app data (files, configs, media). Stored on TrueNAS.
+ - iSCSI (
iscsi-truenas) — for databases (PostgreSQL, MySQL). Block storage.
+
+
+
+
+ Service Tiers
+ Services are organized into tiers that control resource limits and restart priority:
+
+ | Tier | Examples | Priority |
+ | 0-core | Traefik, DNS, VPN, Auth | Highest — never evicted |
+ | 1-cluster | Redis, Prometheus, CrowdSec | High |
+ | 2-gpu | Ollama, Immich ML, Whisper | Medium |
+ | 3-edge | Nextcloud, Paperless, Grafana | Normal |
+ | 4-aux | Dashy, PrivateBin, CyberChef | Low — evicted first under pressure |
+
+
+
+
+ Infrastructure as Code
+ Everything is managed with Terraform (via Terragrunt). Each service has its own stack:
+ stacks/
+ ├── platform/ ← core infra (22 modules)
+ ├── url/ ← URL shortener (Shlink)
+ ├── immich/ ← photo library
+ ├── nextcloud/ ← file storage
+ └── ... (70+ more)
+ Changes go through git: branch → PR → review → merge → CI applies automatically.
+
+
+
+
diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte
new file mode 100644
index 00000000..6f0d1903
--- /dev/null
+++ b/stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte
@@ -0,0 +1,62 @@
+
+ How to Contribute
+
+
+ Workflow
+
+ - Create a branch:
git checkout -b fix/my-change
+ - Make your changes in
stacks/<service>/main.tf
+ - Push and open a PR:
git push -u origin fix/my-change
+ - Viktor reviews and merges
+ - CI applies automatically — Slack notification when done
+
+
+
+
+ What you CAN change
+
+ - Service configurations (image tags, environment variables, resource limits)
+ - New services (add a new stack under
stacks/)
+ - Ingress routes, health probes, replica counts
+
+
+
+
+ What needs Viktor's review
+
+ - CI pipeline changes (
.woodpecker/)
+ - Terragrunt configuration (
terragrunt.hcl)
+ - Secrets configuration (
.sops.yaml)
+ - Core platform modules (
stacks/platform/)
+
+
+
+
+
+
+
+ - Never
kubectl apply/edit/patch — all changes go through Terraform
+ - Never put secrets in code — ask Viktor to add them to the encrypted secrets file
+ - Never restart NFS on TrueNAS — causes cluster-wide mount failures
+ - Never push directly to master — always use a PR
+
+
+
+
+
+ Need a new secret?
+ Comment on your PR: "I need a database password for my-service." Viktor will add it to the encrypted secrets file and push to your branch.
+ Then reference it in your Terraform: var.my_service_db_password
+
+
+
+
diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte
new file mode 100644
index 00000000..8d8fc3d2
--- /dev/null
+++ b/stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte
@@ -0,0 +1,89 @@
+
+ Getting Started
+ Welcome! Follow these steps to get access to the home Kubernetes cluster.
+
+
+ Step 0 — Join the VPN
+ The cluster is on a private network (10.0.20.0/24). You need VPN access first.
+
+ - Install Tailscale for your OS
+ - Run this in your terminal:
+
tailscale login --login-server https://headscale.viktorbarzin.me
+
+ - A browser window will open with a registration URL
+ - Send that URL to Viktor via email (vbarzin@gmail.com) or Slack
+ - Wait for approval (usually within a few hours)
+ - Once approved, test:
ping 10.0.20.100
+
+
+
+
+ Step 1 — Log in to the portal
+ Visit k8s-portal.viktorbarzin.me and sign in with your Authentik account.
+ If you don't have an account yet, ask Viktor to create one.
+
+
+
+ Step 2 — Set up kubectl
+ Run one of these commands in your terminal to install everything automatically:
+ macOS
+ Requires Homebrew. Install it first if you don't have it.
+ bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=mac)
+ Linux
+ bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)
+ Windows
+ Use WSL2 and follow the Linux instructions.
+
+
+
+ Step 3 — Verify access
+ Run this command. It will open your browser for login the first time:
+ kubectl get namespaces
+ You should see output like:
+ NAME STATUS AGE
+default Active 200d
+kube-system Active 200d
+monitoring Active 200d
+...
+ If you get a connection error, make sure your VPN is connected (tailscale status).
+
+
+
+ Step 4 — Clone the repo
+ git clone https://github.com/ViktorBarzin/infra.git
+cd infra
+ This is where all the infrastructure configuration lives.
+
+
+
+ Step 5 — Install your AI assistant (optional)
+ Install Codex CLI for AI-assisted cluster management:
+ npm install -g @openai/codex
+ Codex reads the AGENTS.md file in the repo and knows how to work with the cluster.
+
+
+
+ Step 6 — Your first change
+
+ - Create a branch:
git checkout -b my-first-change
+ - Edit a service file (e.g., change an image tag in
stacks/echo/main.tf)
+ - Commit and push:
git add . && git commit -m "my first change" && git push -u origin my-first-change
+ - Open a Pull Request on GitHub
+ - Viktor reviews and merges
+ - Woodpecker CI automatically applies the change to the cluster
+ - Slack notification confirms it worked
+
+
+
+
+
diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte
new file mode 100644
index 00000000..2d603dfb
--- /dev/null
+++ b/stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte
@@ -0,0 +1,52 @@
+
+ Service Catalog
+ 70+ services running on the cluster. Here are the most commonly used:
+
+
+
+
+ User-Facing Services
+
+
+
+
+
+
+
diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts b/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts
index 7e512020..696ab6dc 100644
--- a/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts
+++ b/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts
@@ -4,7 +4,6 @@ import { readFileSync } from 'fs';
const CLUSTER_SERVER = 'https://10.0.20.100:6443';
const OIDC_ISSUER = 'https://authentik.viktorbarzin.me/application/o/kubernetes/';
const OIDC_CLIENT_ID = 'kubernetes';
-const PORTAL_URL = 'https://k8s-portal.viktorbarzin.me';
export const GET: RequestHandler = async ({ url }) => {
const os = url.searchParams.get('os') || 'mac';
@@ -46,8 +45,6 @@ users:
- --oidc-extra-scope=groups
interactiveMode: IfAvailable`;
- const escapedKubeconfig = kubeconfigContent.replace(/'/g, "'\\''");
-
let script: string;
if (os === 'linux') {
@@ -98,7 +95,7 @@ fi
# Write kubeconfig
mkdir -p ~/.kube
cat > ~/.kube/config-home << 'KUBECONFIG_EOF'
-${escapedKubeconfig}
+${kubeconfigContent}
KUBECONFIG_EOF
echo "[OK] Kubeconfig written to ~/.kube/config-home"
@@ -152,7 +149,7 @@ fi
# Write kubeconfig
mkdir -p ~/.kube
cat > ~/.kube/config-home << 'KUBECONFIG_EOF'
-${escapedKubeconfig}
+${kubeconfigContent}
KUBECONFIG_EOF
echo "[OK] Kubeconfig written to ~/.kube/config-home"
diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte
new file mode 100644
index 00000000..17ac2e5a
--- /dev/null
+++ b/stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte
@@ -0,0 +1,63 @@
+
+ Troubleshooting
+
+
+ "kubectl can't connect to the server"
+
+ - Check your VPN:
tailscale status — should show "connected"
+ - Check KUBECONFIG:
echo $KUBECONFIG — should be ~/.kube/config-home
+ - Test connectivity:
ping 10.0.20.100
+ - If ping works but kubectl doesn't, re-run the setup script
+
+
+
+
+ "Forbidden" or "Permission denied"
+ You may not have access to that namespace. Your access is scoped to specific namespaces.
+ Try: kubectl get namespaces to see which namespaces you can access.
+ Need access to another namespace? Ask Viktor.
+
+
+
+ "Pod is CrashLoopBackOff"
+
+ - Check pod logs:
kubectl logs -n <namespace> <pod-name> --tail=50
+ - Check previous crash:
kubectl logs -n <namespace> <pod-name> --previous
+ - Check events:
kubectl describe pod -n <namespace> <pod-name>
+ - Common causes: OOMKilled (need more memory), bad config, database connection failure
+
+
+
+
+ "PR CI failed"
+
+ - Check the Woodpecker CI dashboard: ci.viktorbarzin.me
+ - Read the build logs — the error is usually at the bottom
+ - Fix the issue, commit, and push — CI will re-run
+
+
+
+
+ "I need a new secret / database password"
+ Secrets are managed by Viktor in an encrypted file. You cannot add them yourself.
+
+ - Comment on your PR: "Need DB password for <service>"
+ - Viktor adds the secret and pushes to your branch
+ - Reference it as
var.<service>_db_password in your Terraform
+
+
+
+
+
+
+
diff --git a/stacks/platform/modules/k8s-portal/main.tf b/stacks/platform/modules/k8s-portal/main.tf
index 40217516..bab83dab 100644
--- a/stacks/platform/modules/k8s-portal/main.tf
+++ b/stacks/platform/modules/k8s-portal/main.tf
@@ -1,5 +1,9 @@
variable "tls_secret_name" {}
variable "tier" { type = string }
+variable "k8s_ca_cert" {
+ type = string
+ default = ""
+}
resource "kubernetes_namespace" "k8s_portal" {
metadata {
@@ -23,8 +27,7 @@ resource "kubernetes_config_map" "k8s_portal_config" {
}
data = {
- # CA cert extracted from kubeconfig — will be populated with cluster CA cert
- "ca.crt" = ""
+ "ca.crt" = var.k8s_ca_cert
}
}
diff --git a/stacks/platform/modules/reverse_proxy/factory/main.tf b/stacks/platform/modules/reverse_proxy/factory/main.tf
index cfbcf9c2..1af42844 100644
--- a/stacks/platform/modules/reverse_proxy/factory/main.tf
+++ b/stacks/platform/modules/reverse_proxy/factory/main.tf
@@ -17,7 +17,6 @@ variable "protected" {
variable "ingress_path" {
type = list(string)
default = ["/"]
- sensitive = true
}
variable "max_body_size" {
type = string