# Quickstart: manual cluster (no Terraform) Gets a real k3s cluster running today, by hand, so you can learn/practice Flux and GitOps immediately instead of waiting on the Terraform/libvirt provider work in `docs/SETUP.md` to be sorted out. One throwaway VM, created directly with `virt-install` rather than the `dmacvicar/libvirt` Terraform provider. When the Terraform track is ready, tear this down (last section) and provision the "real" 3-node cluster properly instead. Shares steps 1–4 of `docs/SETUP.md` as prerequisites — do those first if you haven't: - Step 1: KVM/libvirt installed on the T630. - Step 2: the unprivileged `k8s` user exists, with `~/.ssh/k3s_homelab` generated. - Step 3: `openssl rand -hex 32` isn't needed here (no agents joining, so no cluster token) — skip it. - Step 4: `cloud-demo` pushed to Forgejo, with the `flux-write` token generated. Everything below runs as `k8s` on the T630 (`sudo -iu k8s`). ## 1. Create the VM ```sh mkdir -p ~/vms curl -L -o ~/vms/noble-base.img \ https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img # a writable overlay on top of the base image, sized up to 20G qemu-img create -f qcow2 -F qcow2 -b ~/vms/noble-base.img ~/vms/k3s-manual.qcow2 20G ``` Write the cloud-init user-data — paste in the contents of `~/.ssh/k3s_homelab.pub` where marked: ```sh cat > ~/vms/k3s-manual-user-data.yaml <<'EOF' #cloud-config hostname: k3s-manual manage_etc_hosts: true users: - name: k3s groups: sudo shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL ssh_authorized_keys: - PASTE ~/.ssh/k3s_homelab.pub CONTENTS HERE package_update: true packages: - curl runcmd: - curl -sfL https://get.k3s.io | sh -s - server - mkdir -p /home/k3s/.kube - k3s kubectl config view --raw > /home/k3s/.kube/config - chown -R k3s:k3s /home/k3s/.kube EOF ``` This is a single, standalone server — no join token, no agents, no custom network (uses libvirt's default NAT network + DHCP). A k3s server node schedules workloads on itself by default, so this alone is a complete, usable cluster. ```sh virt-install \ --name k3s-manual \ --memory 2048 \ --vcpus 2 \ --disk ~/vms/k3s-manual.qcow2 \ --import \ --os-variant ubuntu24.04 \ --network network=default \ --cloud-init user-data=~/vms/k3s-manual-user-data.yaml \ --graphics none \ --noautoconsole ``` ## 2. Find its IP and get kubectl talking to it Give cloud-init ~2 minutes to finish installing k3s after the VM boots. ```sh virsh domifaddr k3s-manual # note the IP under the default network ssh k3s@ sudo cat /etc/rancher/k3s/k3s.yaml \ | sed "s/127.0.0.1//" > ~/.kube/config-manual export KUBECONFIG=~/.kube/config-manual kubectl get nodes # expect 1 Ready node ``` ## 3. Bootstrap Flux against Forgejo Same repo, same token, same target path as the full guide would use — nothing about this is cluster-specific: ```sh flux bootstrap git \ --url=https://git.boglabob.com/codegit/cloud-demo \ --branch=main \ --path=clusters/homelab \ --username=codegit \ --password= \ --token-auth \ --kubeconfig ~/.kube/config-manual ``` ## 4. Continue with the shared steps From here, `docs/SETUP.md` steps 8 onward apply exactly as written, regardless of how the cluster was created — verifying the podinfo GitOps loop, registering the Forgejo Actions runner, exposing apps through Caddy, the Dashboard, remote kubectl access. Just use `~/.kube/config-manual` as the kubeconfig throughout instead of the Terraform-provisioned one. ## Tearing this down Once the Terraform track in `docs/SETUP.md` is ready and you `tofu apply` the real 3-node cluster, remove this one: ```sh # as k8s virsh destroy k3s-manual # stop it virsh undefine k3s-manual --remove-all-storage # delete VM + its disk rm ~/.kube/config-manual rm -rf ~/vms ``` Nothing else needs cleaning up — Flux's state lived entirely inside that VM's cluster and goes away with it. The Forgejo repo, deploy tokens, and the Forgejo Actions runner registration are all cluster-independent and carry over to the real cluster unchanged; just re-run `flux bootstrap` against its kubeconfig once it's up.