blumeops/docs/tutorials/replication/kubernetes-bootstrap.md
Erich Blume b197bd5f58 Adopt Dagger CI for docs build (Phase 2) (#157)
## Summary

Migrates the docs build pipeline to Dagger (Phase 2 of the Dagger CI adoption plan).

- **Backfill `date-modified` frontmatter** on all 80 docs — Dagger's `--src=.` excludes `.git`, so Quartz can't use git history for page dates. Frontmatter dates work with or without git.
- **New `docs-check-frontmatter` mise task + pre-commit hook** — validates all docs have `title`, `tags`, and `date-modified`
- **New Dagger functions** — `build_changelog` (towncrier in Python container) and `build_docs` (chains changelog → Quartz build in Node container, returns tarball)
- **Simplified CI workflow** — the ~44-line inline Quartz build (clone, npm ci, build, tar, cleanup) is replaced by `dagger call build-docs`. Changelog step remains local on the runner since towncrier needs to modify the host working tree for the git commit.

### Design decisions

- **Towncrier runs twice in CI**: once inside Dagger (for the docs tarball) and once on the runner (for the git commit). This is intentional — Dagger's directory export is additive and can't delete the consumed changelog fragments from the host.
- **Artifact hosting stays on Forgejo Releases** (not migrated to Forgejo Packages as the plan doc originally suggested). That migration can happen independently.
- **`date-modified` frontmatter** preserved even though `build_changelog` installs git — the git there is only for towncrier's `git add` call, not for history. The local iteration story (`dagger call build-docs --src=. --version=dev` with uncommitted changes) depends on frontmatter dates.

### Local iteration

```bash
dagger call build-docs --src=. --version=dev export --path=./docs-dev.tar.gz
tar tf docs-dev.tar.gz | head -20
```

## Deployment and Testing

- [x] `dagger call build-docs --src=. --version=dev` produces valid 1.1MB tarball (149 HTML pages)
- [x] Pre-commit hooks pass (including new `docs-check-frontmatter`)
- [ ] Full `workflow_dispatch` run after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/157
2026-02-11 16:33:16 -08:00

4.1 KiB

title date-modified tags
Kubernetes Bootstrap 2026-02-07
tutorials
replication
kubernetes

Bootstrapping Kubernetes

Audiences: Replicator

This tutorial walks through setting up a Kubernetes cluster for your homelab, making it accessible via Tailscale.

Choosing a Distribution

For homelab use, lightweight distributions work well:

Distribution Best For BlumeOps Uses
Minikube Single-node, macOS Yes
k3s Single-node, Linux -
kind Local development -
kubeadm Multi-node clusters -

This tutorial uses minikube, but principles apply broadly.

For BlumeOps specifics, see cluster.

Step 1: Install Minikube

macOS

brew install minikube

Linux

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

Step 2: Create the Cluster

minikube start \
  --driver=docker \
  --cpus=4 \
  --memory=8g \
  --disk-size=100g \
  --apiserver-names=k8s.your-tailnet.ts.net,$(hostname) \
  --listen-address=0.0.0.0

Key flags:

  • --apiserver-names - Include your Tailscale hostname for remote access
  • --listen-address=0.0.0.0 - Allow connections from other machines

Step 3: Verify the Cluster

kubectl get nodes
# Should show your node as Ready

kubectl get pods -A
# Should show system pods running

Step 4: Expose via Tailscale

To access the cluster from other Tailscale devices, expose the API server:

Option A: Tailscale Serve (Simple)

tailscale serve --bg --tcp 6443 tcp://localhost:$(minikube ip --format '{{.Port}}')

Option B: Tailscale Kubernetes Operator (Advanced)

For production-like setup, install the Tailscale operator which manages ingress automatically.

BlumeOps uses TCP passthrough via Caddy - see routing.

Step 5: Configure Remote Access

On your workstation, add a context for the remote cluster:

# Copy the CA cert from the server
scp server:~/.minikube/ca.crt ~/.kube/minikube-ca.crt

# Add the cluster
kubectl config set-cluster minikube-remote \
  --server=https://k8s.your-tailnet.ts.net:6443 \
  --certificate-authority=$HOME/.kube/minikube-ca.crt

# Add credentials (copy from server's ~/.kube/config)
kubectl config set-credentials minikube-remote \
  --client-certificate=... \
  --client-key=...

# Add context
kubectl config set-context minikube-remote \
  --cluster=minikube-remote \
  --user=minikube-remote

# Test
kubectl --context=minikube-remote get nodes

Step 6: Storage Configuration

For persistent workloads, configure storage:

Local Path Provisioner (Simple)

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

NFS for Shared Storage

If you have a NAS:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-share
spec:
  capacity:
    storage: 1Ti
  accessModes:
    - ReadWriteMany
  nfs:
    server: nas.your-tailnet.ts.net
    path: /volume1/k8s

What You Now Have

  • A Kubernetes cluster running on your server
  • Remote access via Tailscale
  • Storage for persistent workloads

Next Steps

  • argocd-config - GitOps deployments
  • Install essential addons (ingress controller, cert-manager)

BluemeOps Specifics

BlumeOps' cluster configuration includes:

  • Tailscale operator for automatic ingress
  • NFS mounts from sifaka for media storage
  • CloudNativePG for PostgreSQL databases

See cluster and apps for full details.

Troubleshooting

Problem Solution
Can't connect remotely Check --apiserver-names includes Tailscale hostname
Pods stuck pending Check storage class is available
Connection refused Verify --listen-address=0.0.0.0 was set
Certificate errors Ensure CA cert matches server's