A hands-on Kubernetes homelab built with K3s running on Proxmox VMs and managed using a GitOps workflow with ArgoCD.
The repository contains Kubernetes manifests and infrastructure configuration for deploying and managing the cluster declaratively, including:
- K3s — Lightweight Kubernetes distribution
- Proxmox VE — Virtualization platform hosting the Kubernetes nodes
- ArgoCD — GitOps-based continuous delivery
- Longhorn — Distributed persistent storage
- MetalLB — LoadBalancer implementation for the bare-metal cluster
- GitHub — Source of truth for Kubernetes configuration
The goal is to build a practical Kubernetes environment while learning and documenting cluster operations, GitOps, networking, storage, and workload deployment.
kubernetes-gitops/
├── root-application.yaml
│
├── infrastructure/
│ ├── metallb/
│ │ └── application.yaml
│ └── longhorn/
│ └── application.yaml
│
└── infrastructure-config/
└── metallb/
├── ipaddresspool.yaml
└── l2advertisement.yaml
- 1 VM for master node k3s-master01
(2 vCPU, +8GB RAM, +50GB Storage) - IP Addr 192.168.31.4 (set according to your local subnet) - 1 VM for worker node k3s-worker01
(2 vCPU, +8GB RAM, +50GB Storage) - IP Addr 192.168.31.5 (set according to your local subnet) - Tested on Debian 13 and Ubuntu 26.04 server
-
In
k3s-master01Install K3s master/control plane without servicelb. We will replace it with Metal LB
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable servicelb" sh -s - --write-kubeconfig-mode 644Verify with
systemctl status k3sGet K3s token for worker node installation
cat /var/lib/rancher/k3s/server/node-token -
In
k3s-worker01Install K3s worker/agent
curl -sfL https://get.k3s.io | K3S_URL=https://<mymasternode>:6443 K3S_TOKEN=<mymasternodetoken> sh -<mymasternode>is k3s-master01 IP Address or hostname
<mymasternodetoken>is the token from step 1If you want to run
kubectlin worker node, copy/etc/rancher/k3s/k3s.yamlfrom master node.
Modifyserver: https://127.0.0.1:6443toserver: https://<MASTER_NODE_IP>:6443
Then runmkdir -p ~/.kube mv k3s.yaml ~/.kube/config chmod 600 ~/.kube/config -
From
k3s-master01install ArgoCD to the clusterCheck status both nodes
kubectl get nodesCreate namespace and install ArgoCD
kubectl create namespace argocd kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlWait few minutes to complete ArgoCD deployment, check with
kubectl get all -n argocdGet ArgoCD secret
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echoPort Forwarding for tunneling
kubectl port-forward svc/argocd-server -n argocd 8081:443 -
From you computer
Tunneling to
k3s-master01_IP_ADDRESSwith existingUSERNAMEssh -L 8081:127.0.0.1:8081 USERNAME@k3s-master01_IP_ADDRESSAccess ArgoCD Web UI from web browser with user
adminand secret from step 3https://127.0.0.1:8081
Note
Prepare your own repo as source of ArgoCD to deploy apps, you can clone this repo to your github account.
If you don't want to deploy Longhorn, remove longhorn from infrastructure directory
-
In
k3s-master01create key for git repo. Skip passphrase (leave it empty)ssh-keygen -t ed25519 -f ~/.ssh/argocd-github -C "argocd-kubernetes-gitops"Copy content of public key
cat ~/.ssh/argocd-github.pub -
Add it to GitHub
In your repo example
tobing/kubernetes-gitopsrepository:
Go toSettings → Deploy keys → Add deploy key
Set:
Title: ArgoCD
Key: paste the contents of argocd-github.pub
Allow write access: OFF
We only want ArgoCD to read Git. -
From
k3s-master01create secret:kubectl create secret generic repo-kubernetes-gitops \ -n argocd \ --from-literal=type=git \ --from-literal=url=git@github.com:tobing/kubernetes-gitops.git \ --from-file=sshPrivateKey=$HOME/.ssh/argocd-githubModify
git@github.com:tobing/kubernetes-gitops.gitto your git repo -
Then label it so ArgoCD recognizes it as a repository credential:
kubectl label secret repo-kubernetes-gitops \ -n argocd \ argocd.argoproj.io/secret-type=repository
-
Check
infrastructure-config/metallb/ipaddresspool.yamlfor your Metal LB IP Address pool.
I am using192.168.31.240-192.168.31.250. Set them based on your local subnet. -
Create a temporary file
root-application.yaml⚠️ This file for ArgoCD initialization.
ModifyrepoURLaccording to your git repo.$\color{red}{\text{CHECK CAREFULLY}}$ Run
kubectl apply -f root-application.yamland check ArgoCD Web UI.After few minutes from
k3s-master01check if all running and no issuekubectl get all -A$\color{red}{\text{If you did not remove longhorn from infrastructure directory in your github repo, you will deploy longhorn also}}$ $\color{red}{\text{Make sure to follow Longhorn instruction in the bottom}}$ -
Try to modify metallb version
targetRevision: 0.16.1ininfrastructure/metallb/application.yamlto something else like0.16.0.
After git push, ArgoCD will syncing.
Note
Longhorn - Distributed Block Storage System for Kubernetes
Details
-
Install iSCSI in both
k3s-master01andk3s-worker01apt update apt install -y open-iscsiEnable the services
systemctl enable --now iscsid systemctl enable --now open-iscsiCheck
systemctl status iscsid.socket --no-pager systemctl status iscsid --no-pager -
Because we use 2 nodes only, but Longhorn default replica is 3
kubectl -n longhorn-system get settings.longhorn.io default-replica-count -o yamlvalue: '{"v1":"3","v2":"3"}'Change to 2
kubectl -n longhorn-system patch settings.longhorn.io default-replica-count \ --type=merge \ -p '{"value":"{\"v1\":\"2\",\"v2\":\"2\"}"}'Verify
kubectl -n longhorn-system get settings.longhorn.io default-replica-count -o yaml -
Create a test Persistant Volume Claim (PVC)
kubectl create -f - <<'EOF' apiVersion: v1 kind: PersistentVolumeClaim metadata: name: longhorn-test spec: storageClassName: longhorn accessModes: - ReadWriteOnce resources: requests: storage: 2Gi EOFCheck
kubectl get pvc longhorn-testResult
STATUS Bound -
Create a test pods
kubectl create -f - <<'EOF' apiVersion: v1 kind: Pod metadata: name: longhorn-test spec: containers: - name: test image: busybox command: ["sh", "-c", "echo 'Longhorn works!' > /data/test.txt && sleep 3600"] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: longhorn-test EOFCheck if running
kubectl get pod longhorn-testIf running
kubectl exec longhorn-test -- cat /data/test.txtExpected result
Longhorn works!To delete the pod
kubectl delete pod longhorn-test -
Access Longhorn Web UI
From
k3s-master01kubectl port-forward -n longhorn-system svc/longhorn-frontend 8090:80From you computer tunneling to
k3s-master01_IP_ADDRESSwith existingUSERNAMEssh -L 8090:127.0.0.1:8090 USERNAME@k3s-master01_IP_ADDRESSAccess Longhorn Web UI from web browser
http://127.0.0.1:8090 -
Troubleshooting "Degraded" volume because step 13
kubectl -n longhorn-system get volumekubectl -n longhorn-system get volumes.longhorn.io <pvc-id> -o jsonpath='{.spec.numberOfReplicas}{"\n"}'kubectl -n longhorn-system patch volumes.longhorn.io <pvc-id> --type=merge -p '{"spec":{"numberOfReplicas":2}}'