---
title: "Setup K3s Single Node Cluster, GitOps with Flux"
description: "As I mentioned previously here and here,\nI'm developing a personal Homelab to support my daily development workflow.\nOne of the important components of this setup is how I manage application deploymen..."
publishedAt: 2026-02-20
locale: en
urlSlug: setup-k3s-single-node-gitops-dengan-flux
isDraft: false
defaultLocale: en
---
[TOC]

## Background

As I mentioned previously [here](membangun-local-kubernetes-development-dengan-lima-vm) and [here](linux-minipc-untuk-remote-dev),
I'm developing a personal Homelab to support my daily development workflow.
One of the important components of this setup is how I manage application deployments
to the Kubernetes cluster in a more declarative way using the GitOps concept.

GitOps is a way to manage infrastructure and applications through Git, where the desired state
is defined in a repository and tools like Flux will ensure the cluster matches that definition.

There are several popular GitOps tools today such as [ArgoCD](https://argo-cd.readthedocs.io/) and [Flux](https://fluxcd.io/).
I chose Flux because of its native integration with SOPS for secret encryption, as well as its image automation
capability that allows automatic deployment when a new Docker image is pushed to the registry.

However, I can't share my setup 1:1 because I made the GitHub repo private.
So I'm trying to replicate how I did the initial setup, perhaps some friends here are interested in building something similar wqwq.

## Requirements
Install:
- lima-vm
- kubectl
- sops
- flux
- [age](https://github.com/FiloSottile/age)

All the tools above can be installed manually or using [arkade](https://github.com/alexellis/arkade),
I myself am comfortable using arkade. For example:

```bash
arkade get kubectl flux sops
```

regarding lima-vm, you can check my previous writing [membangun-local-kubernetes-development-dengan-lima-vm](membangun-local-kubernetes-development-dengan-lima-vm).

## Setup K3s Single Node

First create the lima-vm instance
```bash
limactl create template:k3s \
    --name=k3s-demo \
    --cpus=2 \
    --memory=2 \
    --disk=20 \
    --yes
```

after the instance is created, the output at the end will show the kubeconfig file that will be used to manage the cluster via `kubectl`

```bash
export KUBECONFIG="/home/jimbo/.lima/k3s-demo/copied-from-guest/kubeconfig.yaml"
kubectl...
```

### Environment Variable Management

make sure to export the variable above before executing `kubectl` related commands, for convenience I suggest using [direnv](https://github.com/direnv/direnv)

create a `.direnv` file like this
```bash
export KUBECONFIG="/home/jimbo/.lima/k3s-demo/copied-from-guest/kubeconfig.yaml"
```

all necessary variables, we can add later as needed in the file above.

### Check connection to k3s cluster with kubectl

Execute the following command
```bash
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:6443
CoreDNS is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
```

## Installing Flux CD

Create a git repo, which later this repo will be the source of truth for the k8s state we manage, here is the example repo I made https://github.com/wayanjimmy/k3s-demo.

Now we need to bootstrap fluxcd to the k3s cluster.
```bash
flux bootstrap git \
    --url=ssh://git@github.com/wayanjimmy/k3s-demo.git \
    --private-key-file=/home/jimbo/.ssh/id_ed25519 \
    --branch=main \
    --path=clusters/k3s-demo \
    --components-extra image-reflector-controller,image-automation-controller
```

make sure the ssh key we use is already registered in github, so the bootstrap process can access the repo, after bootstrap is successful, execute the following command to make sure flux is ready.

```bash
$ flux check
 ► checking prerequisites
 ✔ Kubernetes 1.34.4+k3s1 >=1.32.0-0
 ► checking version in cluster
 ✔ distribution: flux-v2.7.5
 ✔ bootstrapped: true
 ► checking controllers
 ✔ helm-controller: deployment ready
 ► ghcr.io/fluxcd/helm-controller:v1.4.5
 ✔ image-automation-controller: deployment ready
 ► ghcr.io/fluxcd/image-automation-controller:v1.0.4
 ✔ image-reflector-controller: deployment ready
 ► ghcr.io/fluxcd/image-reflector-controller:v1.0.4
 ✔ kustomize-controller: deployment ready
 ► ghcr.io/fluxcd/kustomize-controller:v1.7.3
 ✔ notification-controller: deployment ready
 ► ghcr.io/fluxcd/notification-controller:v1.7.5
 ✔ source-controller: deployment ready
 ► ghcr.io/fluxcd/source-controller:v1.7.4
 ► checking crds
 ✔ alerts.notification.toolkit.fluxcd.io/v1beta3
 ✔ buckets.source.toolkit.fluxcd.io/v1
 ✔ externalartifacts.source.toolkit.fluxcd.io/v1
 ✔ gitrepositories.source.toolkit.fluxcd.io/v1
 ✔ helmcharts.source.toolkit.fluxcd.io/v1
 ✔ helmreleases.helm.toolkit.fluxcd.io/v2
 ✔ helmrepositories.source.toolkit.fluxcd.io/v1
 ✔ imagepolicies.image.toolkit.fluxcd.io/v1
 ✔ imagerepositories.image.toolkit.fluxcd.io/v1
 ✔ imageupdateautomations.image.toolkit.fluxcd.io/v1
 ✔ kustomizations.kustomize.toolkit.fluxcd.io/v1
 ✔ ocirepositories.source.toolkit.fluxcd.io/v1
 ✔ providers.notification.toolkit.fluxcd.io/v1beta3
 ✔ receivers.notification.toolkit.fluxcd.io/v1
 ✔ all checks passed
```

if you want to check the flux related workload you can use the following command

```bash
$ kubectl get pods -n flux-system
NAME                                       READY   STATUS    RESTARTS   AGE
helm-controller-68578f8447-t6v2k           1/1     Running   0          64s
kustomize-controller-7ddfbb5875-jvlvm      1/1     Running   0          64s
notification-controller-6d766f87cf-5lrvf   1/1     Running   0          64s
source-controller-6679d8bdb-dgdgs          1/1     Running   0          64s
```

at this point flux is installed, continue to the app deployment process.

## Testing Deploy Demo Application

We will write yaml using kustomization, which is a kind of abstraction layer for k8s yaml management, but will not be discussed in this writing.

Create the following file.

clusters/k3s-demo/demo-apps-kustomization.yaml
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: demo-apps
  namespace: flux-system
spec:
  interval: 1m
  path: ./clusters/k3s-demo/demo-apps
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  # decryption:
  #   provider: sops
  #   secretRef:
  #     name: sops-age
```

clusters/k3s-demo/demo-apps/namespace.yaml
```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: demo-apps
```

clusters/k3s-demo/demo-apps/nginx-demo.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
  namespace: demo-apps
  labels:
    app: nginx-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo
  namespace: demo-apps
spec:
  type: NodePort
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30081
```

clusters/k3s-demo/demo-apps/kustomization.yaml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo-apps
resources:
- namespace.yaml
- nginx-demo.yaml
# - secret.yaml
```

The nginx-demo deployment is already exposed via nodeport so later we can check through that port whether the application runs normally.

Well how to deploy this application through the git ops flow?
Flux will periodically, based on the interval `clusters/k3s-demo/demo-apps-kustomization.yaml`, ensure whether the git repo definition is sync with the actual state.

What we need to do is just commit all these yaml files and push to git, here is the repo condition picture after doing all the files above committed [https://github.com/wayanjimmy/k3s-demo/tree/81635f0f880c53a63bf8c8c53752b5d1b873a416/clusters/k3s-demo](https://github.com/wayanjimmy/k3s-demo/tree/81635f0f880c53a63bf8c8c53752b5d1b873a416/clusters/k3s-demo)

Wait a few moments and flux will reconcile its state and the pod will appear

```bash
$ kubectl get pods -n demo-apps
NAME                          READY   STATUS    RESTARTS   AGE
nginx-demo-6fd7fbb8f4-4jhjj   1/1     Running   0          136m
```


## Encrypt Secret with SOPS

Flux has integration with sops that we can use so that secrets committed to the git repo are not naked.
For further reference about Flux and SOPS, you can check the writing of [Budiman JoJo](https://budimanjojo.com/2021/10/23/flux-secret-management-with-sops-age/).

```bash
mkdir -p secrets
```

add .gitignore in the secrets folder to avoid the private key we generate from being committed.

secrets/.gitignore
```text
*
!.gitignore
```

Generate sops private & public key pair
```bash
$ age-keygen -o secrets/flux-age-key.txt
```

if you check the structure of the file `flux-age-key.txt` it will be like this

```text
# created: 2026-02-20T07:19:42Z
# public key: age1h44rlvy9c2ffytt78msmhjtuyuwtpyc6jje7fgjkg4x7l0hpt5dsfxlf9v
AGE-SECRET-KEY-1ECFASJKZTGVTM0K2GV4QYUXGSKL0S2DG43SWV68GWLC455LAFMYQSLGHQA
```

there is a public key and secret key, public key can be added in the repo so everyone can do encryption, while private will be added as a secret in the k8s cluster, so the cluster can do decryption.

```bash
$ kubectl create secret generic sops-age \
     --namespace=flux-system \
     --from-file=age.agekey=$(pwd)/secrets/flux-age-key.txt
secret/sops-age created
```

Make sure the sops-age secret is created

```bash
$ kubectl get secret sops-age -n flux-system
NAME       TYPE     DATA   AGE
sops-age   Opaque   1      2m28s
```

Ok at this stage we just need to introduce the demo-apps kustomization to recognize sops-age as the key to decrypt secret, remove the comment mark related to decryption that was previously in the comment tag.

clusters/k3s-demo/demo-apps-kustomization.yaml
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: demo-apps
  namespace: flux-system
spec:
  interval: 1m
  path: ./clusters/k3s-demo/demo-apps
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  decryption:                       # [!code ++]
    provider: sops                  # [!code ++]
    secretRef:                      # [!code ++]
      name: sops-age                # [!code ++]
```

commit all changes and push and wait until flux reconciles, you can watch with the flux command to be more sure.

```bash
$ flux get kustomizations -n flux-system --watch
NAME            REVISION                SUSPENDED       READY   MESSAGE
demo-apps       main@sha1:f33a03f7      False           True    Applied revision: main@sha1:f33a03f7
flux-system     main@sha1:f33a03f7      False   True    Applied revision: main@sha1:f33a03f7
```

If already delete pods so the new configuration is applied in the new pod

```bash
$ kubectl delete pods -l app=demo-app -n demo-apps
```

### Ensuring Pod can Decrypt Secret

At this point we just need to ensure whether the demo-apps workload can decrypt against the secret that has been encrypted using sops.

Create secret file.

```bash
$ cat clusters/k3s-demo/demo-apps/secret.yaml
apiVersion: v1
stringData:
  database: somedatabase
  password: secret
  username: admin
kind: Secret
metadata:
  name: db-credentials
  namespace: demo-apps
```

Encrypt secret file with sops

```bash
$ sops --encrypt \
    --age $(echo $SOPS_PUBLIC_KEY) \
    --encrypted-regex '^(data|stringData)$' \
    --in-place clusters/k3s-demo/demo-apps/secret.yaml
```

after that the file will be encrypted like [this](https://github.com/wayanjimmy/k3s-demo/blob/main/clusters/k3s-demo/demo-apps/secret.yaml)

register secret

clusters/k3s-demo/demo-apps/kustomization.yaml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo-apps
resources:
- namespace.yaml
- nginx-demo.yaml
- secret.yaml                    # [!code ++]
```

Well how to make sure the pod can access secret in decrypted state?
We can add initContainers which this container will be executed first before the nginx container in nginx-demo is executed.

clusters/k3s-demo/demo-apps/nginx-demo.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
  namespace: demo-apps
  labels:
    app: nginx-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      initContainers:                    # [!code ++]
      - name: secret-test                # [!code ++]
        image: busybox:1.36             # [!code ++]
        command:                         # [!code ++]
        - sh                             # [!code ++]
        - -c                             # [!code ++]
        - |                              # [!code ++]
          echo "=== SOPS Secret Decryption Test ==="
          echo "DB_HOST: $DB_HOST"
        envFrom:                         # [!code ++]
        - secretRef:                     # [!code ++]
            name: db-credentials         # [!code ++]
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo
  namespace: demo-apps
spec:
  type: NodePort
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30081
```

Commit all changes and push, wait for flux to reconcile, you can watch the log of the init containers earlier with the command

```bash
jimbo@ser4:~/.../k3s-demo$ kubectl logs -f -l app=nginx-demo -c secret-test -n demo-apps
=== SOPS Secret Decryption Test ===
DB_HOST: postgres.demo-apps.svc.cluster.local
```

there you will see that the env `DB_HOST` can be accessed in decrypted state, while the secret in the git repo is also safe in encrypted state.

At this point the flux setup process with sops is complete!

## Conclusion

So far we have a single-node Kubernetes cluster managed with GitOps
using Flux. With this setup, all application deployments can be done just with
git push, while secrets remain safe with SOPS encryption.

The combination of k3s + lima-vm is very suitable for tinkering with Homelab without having to have special hardware,
everything can run on the laptop we use daily.
