Setting Up GitOps on your K3s HomeLab with Argo CD
21 May 2026 · 5.335 min read

argo-cd

A while ago I built a small Kubernetes cluster using four Raspberry Pi 4s for local development and experimentation. I then added a private Docker registry so I could build and deploy my own container images locally without depending on Docker Hub.

With that setup I could build containers, push images and deploy workloads - but deployments were still manual. That meant I was applying manifests directly with kubectl, tweaking YAML locally, and wondering whether the cluster state actually matched what was in Git.

Argo CD + GitOps

argo-cd

Argo CD is a GitOps continuous delivery platform for Kubernetes that keeps your cluster state synchronised with what’s defined in Git. Instead of manually applying manifests with kubectl, you declare applications, infrastructure, and configuration in a repository and Argo CD continuously reconciles the cluster towards that desired state. In practice, it gives you automated deployments, drift detection, self-healing, rollback capability, and a much clearer operational model where Git becomes the source of truth for your platform.

Installing Argo CD

Installing Argo CD on k3s is surprisingly straightforward.

First create the namespace:

kubectl create namespace argo-cd

Then apply the official install manifest:

kubectl apply -n argo-cd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

After a minute or two you should see the Argo CD pods starting:

kubectl get pods -n argo-cd

Exposing the Argo CD UI

Now, to expose Argo CD using an ingress, create argo-cd/infrastructure/ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd
  namespace: argo-cd
spec:
  ingressClassName: traefik
  rules:
    - host: argo-cd.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80

…and apply it to your cluster with:

kubectl apply -f argo-cd/infrastructure/ingress.yaml

Update your /etc/hosts:

192.168.1.10 argo-cd.local

At this point you should be able to log into the Argo CD UI at http://argo-cd.local:

argo-cd

Note that you'll need to retrieve the initial admin password with:

kubectl -n argo-cd \
  get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d

Setting up a GitOps Repository

GitOps is an operational approach where Git becomes the source of truth for infrastructure and application deployment configuration. Instead of manually changing environments, you declare the desired state in version-controlled repositories and automated systems continuously reconcile the running platform to match. In practice, this gives you reproducible deployments, auditability, safer rollbacks, reduced configuration drift, and a much more predictable way to manage Kubernetes and cloud-native systems.

So we keep cluster state separate from application source code instead of putting Kubernetes manifests inside each application. There are endless opinions online about the “correct” GitOps structure but my GitOps repository is shaped like this:

gitops/
├── apps/
│   ├── an-app/
│   │   ├── deployment.yaml
│   │   ├── ingress.yaml
│   │   └── service.yaml
│   │
│   └── another-app/
│
└── argo-cd/
    ├── applications/
    │   ├── an-app.yaml
    │   └── another-app.yaml
    └── infrastructure/
        ├── ingress.yaml
        └── sealed-secret.yaml ← don't commit gitops-secrets.yaml!

Configuring Argo CD with GitOps Credentials

If your GitOps repository is private, Argo CD needs credentials so it can read the manifests it is responsible for syncing into the cluster. The simplest way to do this is with a standard Kubernetes secret in the argo-cd namespace.

For a GitHub repository, you can create a personal access token and store it as an Argo CD repository secret, so create gitops-secrets.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: gitops-repo-credentials
  namespace: argo-cd
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
stringData:
  type: git
  url: https://github.com/chrisallmark/gitops.git
  username: chrisallmark
  password: <your-github-token>

… and apply it with:

kubectl apply -f gitops-secrets.yaml

Once this secret exists, Argo CD can authenticate against the private repository and pull the Kubernetes manifests defined in your GitOps repo and from there, your Application resources can reference the repo as normal.

PRO TIP: For a local cluster this is enough to get started, but it does mean the token is sitting in plain text in a YAML file before it is applied. That is fine for a quick local experiment, but I would not commit this file to Git. A better next step is to use Sealed Secrets, which lets you encrypt the secret and safely store the encrypted version in your GitOps repository.

Creating an Argo CD Application

An Argo CD application defines where manifests live, which cluster/namespace to deploy into and how reconciliation should behave.

Create your application manifests and commit them to your GitOps repository, for example:

apps/an-app/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: an-app
  namespace: apps
spec:
  replicas: 1
  selector:
    matchLabels:
      app: an-app
  template:
    metadata:
      labels:
        app: an-app
    spec:
      containers:
        - name: an-app
          image: k3s:5000/an-app:1.0.0
          ports:
            - containerPort: 3000

apps/an-app/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: an-app
  namespace: apps
spec:
  ingressClassName: traefik
  rules:
    - host: an-app.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: an-app
                port:
                  number: 80

apps/an-app/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: an-app
  namespace: apps
spec:
  selector:
    app: an-app
  ports:
    - port: 80
      targetPort: 3000

Now create an Argo CD application manifest, for example argo-cd/applications/an-app.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: an-app
  namespace: argo-cd
spec:
  project: default
  source:
    repoURL: https://github.com/chrisallmark/gitops.git
    targetRevision: main
    path: apps/an-app
  destination:
    server: https://kubernetes.default.svc
    namespace: apps
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

…and apply it with:

kubectl apply -f argo-cd/applications/an-app.yaml

Argo CD will poll your GitOps repository and start reconciling the application automatically - no more manual kubectl apply and no drift.

Finally update your /etc/hosts

192.168.1.10 argo-cd.local an-app.local

…and hit http://an-app.local to view your app.

Finally…

Before GitOps the cluster still felt fairly experimental. Deployments were procedural, changes were often applied manually, and recovering the environment relied a little too heavily on memory and terminal history. It worked, but there was always a lingering question of whether the cluster state actually matched what was sitting in Git.

After moving to GitOps the whole setup feels much more declarative and predictable. Environments are reproducible, changes are versioned and auditable, and deployments become almost boring - which, operationally, is exactly what you want. The real value is not automation for its own sake. It is reducing ambiguity, drift, and operational guesswork so the platform behaves consistently and predictably over time.

I’m also increasingly interested in whether a small RPi cluster can act as a genuinely useful local platform engineering sandbox rather than just a Kubernetes science project.

So far, surprisingly… yes.