A Three-Node k3s Cluster with GitOps
A real, multi-node Kubernetes cluster on lightweight k3s, deploying workloads automatically from a git repo instead of `kubectl apply`-ing by hand.
What you'll build and why
k3s is a lightweight, certified Kubernetes distribution that ships as a single binary -- a real, standards-compliant cluster without the resource overhead of a full upstream install, which is exactly why it's a common homelab and edge-computing choice. This guide joins three nodes into one cluster, then adds Argo CD so that deploying a workload means committing a YAML change to git, not running kubectl apply by hand from your laptop.
Don't build this if: you're running a handful of services and Docker Compose already covers your needs -- Kubernetes brings real orchestration power (self-healing, scheduling across nodes, rolling updates) at the cost of real added complexity. This guide is for when you specifically want multi-node orchestration, not a Docker Compose replacement by default.
How it works
Node 1 (control-plane + server)
│
│ joins via a shared token
├──── Node 2 (agent/worker)
└──── Node 3 (agent/worker)
Workloads get scheduled across all three nodes
by the Kubernetes scheduler, not manually placed
─────────────────────────────────────────
git repo (your workload YAML)
│
│ Argo CD watches this repo continuously
▼
Argo CD (running in the cluster)
│
│ reconciles: applies any diff between
│ the repo and the live cluster state
▼
Live cluster state
GitOps inverts the usual flow: instead of you pushing changes to the cluster, Argo CD pulls from git and pushes to the cluster itself -- git becomes the single source of truth, and the live cluster is just Argo CD's best current attempt to match it.
Before you start
Decision: three nodes, this specific layout. This guide uses one control-plane node and two workers as the minimum layout that demonstrates real multi-node scheduling and survives losing a single worker. A single-node k3s install (common for just trying it out) skips the multi-node concepts this guide is specifically about.
Steps
Step 1: Install k3s on the first node (server/control-plane)
$ curl -sfL https://get.k3s.io | sh -
Expected output: k3s installs and starts as a systemd service. Confirm with:
$ sudo k3s kubectl get nodes
You should see the one node, Ready.
Step 2: Get the join token and server address
$ sudo cat /var/lib/rancher/k3s/server/node-token
Note this token and the control-plane node's IP -- both are needed to join the other nodes.
Step 3: Join the worker nodes
On each of the two worker nodes:
$ curl -sfL https://get.k3s.io | K3S_URL=https://<control-plane-ip>:6443 K3S_TOKEN=<the-token-from-step-2> sh -
Step 4: Confirm the cluster
Back on the control-plane node:
$ sudo k3s kubectl get nodes
Expected output: all three nodes listed, all Ready.
Step 5: Set up kubeconfig access from your own machine
$ scp <control-plane-user>@<control-plane-ip>:/etc/rancher/k3s/k3s.yaml ~/.kube/config
Edit the copied file's server: line to use the control-plane's actual IP instead of 127.0.0.1. Confirm with kubectl get nodes run from your own machine now.
Step 6: Install Argo CD
$ kubectl create namespace argocd
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
The official install manifest, applied directly per Argo CD's own documented method.
Step 7: Access the Argo CD UI and log in
$ kubectl port-forward svc/argocd-server -n argocd 8080:443
Visit https://localhost:8080. Initial admin password:
$ kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
Log in as admin with that password, then change it.
Step 8: Point Argo CD at your git repo
Create an Argo CD Application resource pointing at a repo containing your workload manifests:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homelab-apps
namespace: argocd
spec:
project: default
source:
repoURL: <your-git-repo-url>
targetRevision: main
path: manifests
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
selfHeal: true is the key GitOps behavior: if someone manually changes the live cluster (a stray kubectl edit), Argo CD reverts it back to match git automatically.
Verify it works
-
kubectl get nodesshows all three nodesReady - Deploying a test workload (a basic
Deployment) viakubectlgets scheduled onto a worker node, not always the control-plane, confirming real multi-node scheduling - Argo CD's UI shows your
ApplicationasSyncedandHealthyafter committing a manifest to the watched repo path - Failure test (GitOps self-heal): manually change a deployed resource with
kubectl edit(e.g. change a replica count) and confirm Argo CD reverts it back to match git within its sync interval -- this is the actual point of GitOps, proven directly - Failure test (node loss): cordon or shut down one worker node and confirm workloads scheduled on it get rescheduled onto the remaining nodes (for workloads with more than one replica) -- single-replica workloads will show downtime, which is expected and worth understanding, not a bug
Secure it
kubectlaccess is cluster-admin by default: the kubeconfig from Step 5 grants full cluster control -- protect it like root access, because it is.- What's exposed: the Kubernetes API server (port 6443) and Argo CD's UI should stay LAN-only in this guide's scope -- neither is set up here with the hardening needed for safe internet exposure.
- Default credentials: Argo CD's initial admin password (Step 7) must be changed immediately -- don't leave the auto-generated one in place.
- GitOps means git access is cluster access: anyone who can merge into the watched repo path can change your cluster's live state, since Argo CD applies whatever's there -- protect that repo's write access accordingly.
Back it up and maintain it
What matters: the git repo itself (Step 8) is your actual source of truth for workload state -- back it up like any other important repo. etcd (k3s's embedded datastore) holds cluster state; k3s includes built-in snapshot support for it, worth enabling for anything beyond a pure lab.
Update cadence: k3s and Argo CD both ship regularly; update deliberately, reading release notes -- a Kubernetes version upgrade can deprecate API versions your manifests use, worth checking before upgrading.
What to monitor: node Ready status (kubectl get nodes); Argo CD's own Application sync/health status, ideally wired into the alerting guide elsewhere in this section rather than checked manually.
Troubleshooting
Logs: sudo journalctl -u k3s (control-plane) or -u k3s-agent (workers); kubectl logs -n argocd <argocd-pod> for Argo CD's own components.
| Symptom | Likely cause | Diagnostic | Fix |
|---|---|---|---|
A worker node never joins / doesn't appear in get nodes |
Wrong token, wrong server URL, or a firewall blocking port 6443 between nodes | sudo journalctl -u k3s-agent on the worker for the actual connection error |
Fix the token/URL; open port 6443 between nodes |
A node shows NotReady |
The k3s service crashed or stopped on that node, or a network/resource issue | sudo journalctl -u k3s (or k3s-agent) on that specific node |
Restart the k3s service; investigate resource pressure if it recurs |
Argo CD Application shows OutOfSync and stays that way |
Sync isn't automated, or there's a genuine error applying the manifest | Check the Application's own events/status in the Argo CD UI for the specific error |
Fix the manifest error, or enable automated sync if intentionally manual |
Workload pod stuck in Pending |
Insufficient resources on any node to schedule it, or a node selector/taint mismatch | kubectl describe pod <pod> shows the scheduler's specific reason |
Free up resources, or adjust the workload's resource requests/scheduling constraints |
selfHeal doesn't revert a manual change |
Automated sync/self-heal isn't actually enabled on that Application, or the sync interval hasn't elapsed yet |
Check the Application's syncPolicy in its YAML |
Confirm automated.selfHeal: true is actually set; wait for the next sync cycle |
Undo
On each node: /usr/local/bin/k3s-uninstall.sh (control-plane) or /usr/local/bin/k3s-agent-uninstall.sh (workers) -- k3s ships these removal scripts itself. This removes the cluster entirely and is not reversible; back up anything from etcd or workload data first if it matters.
Go further
- Add a local storage provisioner or NFS-backed persistent volumes for workloads that need to keep data across pod restarts (this guide's workloads are assumed stateless)
- Wire Argo CD notifications into the alerting stack from the observability guides elsewhere in this section
- Explore multiple Argo CD
Applicationresources (orApplicationSet) as your deployed workload count grows beyond what one flat manifest directory comfortably holds
Resources
Official documentation:
- k3s documentation -- install and multi-node setup, verified this session
- Argo CD documentation -- verified this session
Source and releases:
Community:
Related DaemonPress projects:
- Managing a Homelab with Docker Compose, Portainer, and Watchtower
- Automating the Homelab with Ansible
Last verified: 2026-09-21, checked against official k3s and Argo CD documentation.