This project stores configurations of services in my homelab.
- Add more nodes for a high-availability setup
- Implement solution for backup to offsite storage
- Include IaC (Infrastructure as Code) for server setup (OS & packages)
- Add and improve documentations
- Add CI/CD to lint and sync the configurations
The homelab runs on a single machine with the following specifications:
- Intel i5-3330
- 16GB RAM (8GB+8GB)
- 960GB SSD (OS +
/mnt/fast) - 1TB HDD (
/mnt/bulk) - Nvidia 1050Ti
The operating system of choice is Debian 13 (trixie), with tailscale installed.
The rest of the host is set up with Ansible (ansible/), which installs:
- the NVIDIA driver from Debian's
non-free(the 550 branch, which still supports Pascal GPUs) and the NVIDIA container toolkit, which k3s detects as thenvidiaruntime - a single-node k3s cluster, with the following optional addons disabled:
- helm-controller
- servicelb
- traefik
- local-storage (replaced by a self-managed local-path-provisioner)
- metrics-server
- helm (through Homebrew), used to render charts when bootstrapping Argo CD
Third-party apps/services:
- GitOps solution of choice is combination of kustomize and argo-cd
- No secrets are stored in git. The Tailscale operator OAuth secret is created by hand (see Bootstrap).
- In the Tailscale admin console:
- Enable MagicDNS and HTTPS certificates
- Add the operator's default tags to the policy file:
and, if the policy doesn't allow all traffic, a grant letting your devices reach
tag:k8sandtag:k8s-operatoron port 443 - Give tailnet admins cluster-admin through the operator's API server proxy:
"grants": [{ "src": ["autogroup:admin"], "dst": ["tag:k8s-operator"], "app": { "tailscale.com/cap/kubernetes": [{ "impersonate": { "groups": ["system:masters"] } }] }, }],
- Create an OAuth client (Settings → Trust credentials) tagged
tag:k8s-operator, with write access to Devices Core, Auth Keys and Services, as in the operator install guide
- Set up the host. This needs Ansible and Homebrew installed on the host first.
Debian's
ansiblepackage includes thecommunity.generalcollection; with plainansible-core, runansible-galaxy collection install -r requirements.yamlas well. Add--connection=localwhen running on the host itself:sudo apt install ansible cd ansible && ansible-playbook site.yaml --ask-become-pass - Bootstrap the cluster from the host (no sudo needed;
kubectlandhelmmust be on thePATH):The playbook skips any step that's already done:cd ansible && ansible-playbook bootstrap.yaml- prompts for the Tailscale OAuth client ID and secret (the secret is hidden and never logged)
and creates the
operator-oauthsecret - installs Argo CD and the ApplicationSet, which creates one application per directory under
kustomize/ - syncs
local-path-provisioner,tailscale,metrics-server,argocdandhomelabin order, waiting for each to become healthy. Only applications that have never been synced are synced, so re-running it never forces a sync.
- prompts for the Tailscale OAuth client ID and secret (the secret is hidden and never logged)
and creates the
- Sync the remaining applications by hand in Argo CD, at
https://argocd.<tailnet>.ts.net(useradmin, password fromkubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d).
All endpoints are private and only reachable over Tailscale.
Each Ingress uses the tailscale ingress class from the Tailscale operator,
which gives the service its own tailnet device at https://<name>.<tailnet>.ts.net with a certificate issued by Tailscale.
MagicDNS and HTTPS must be enabled for the tailnet.
Volumes are provisioned by local-path-provisioner into <disk>/k8s/<namespace>/<pvc>/, with reclaimPolicy: Retain:
local-fast(default):/mnt/faston the SSD, for app configlocal-bulk:/mnt/bulkon the HDD, for media (themediaPVC shared by jellyfin and qbittorrent)
The Tailscale operator runs an API server proxy in auth mode.
Requests are authenticated with the caller's Tailscale identity and mapped to Kubernetes groups by the policy grant above,
so no Kubernetes credentials leave the host. From any tailnet device with kubectl installed:
tailscale configure kubeconfig tailscale-operator
On a machine without the tailscale CLI, such as WSL with Tailscale running on Windows, create the same kubeconfig by hand.
The token is a placeholder; the proxy authenticates the connection's Tailscale identity instead:
kubectl config set-cluster homelab --server=https://tailscale-operator.<tailnet>.ts.net
kubectl config set-credentials tailscale-auth --token=unused
kubectl config set-context homelab --cluster=homelab --user=tailscale-auth
kubectl config use-context homelab
If WSL can't resolve or reach tailnet names, enable networkingMode=mirrored and dnsTunneling=true under [wsl2] in
%USERPROFILE%\.wslconfig, then run wsl --shutdown.
The first request after the operator starts may time out while the proxy gets its certificate. If the cluster can't run pods, the proxy is unavailable too; SSH to the host and use its local kubeconfig instead.