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 nodes shows all three nodes Ready
  • Deploying a test workload (a basic Deployment) via kubectl gets scheduled onto a worker node, not always the control-plane, confirming real multi-node scheduling
  • Argo CD's UI shows your Application as Synced and Healthy after 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

  • kubectl access 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 Application resources (or ApplicationSet) as your deployed workload count grows beyond what one flat manifest directory comfortably holds

Resources

Official documentation:

Source and releases:

Community:

Related DaemonPress projects:


Last verified: 2026-09-21, checked against official k3s and Argo CD documentation.