← Back to Kubernetes case study

ENGINEERING JOURNAL

Bootstrapping GitOps Storage and the First Application

A field note from moving a three-node Kubernetes home lab beyond cluster bootstrap and into a repeatable GitOps operating model. This phase established Argo CD application management, version-pinned Helm deployments, dynamic persistent storage, the first stateful service, and Discord-based operational alerting.

Argo CD GitOps Helm Persistent Storage Uptime Kuma Discord Alerting

The cluster needed an operating model, not another manual installation.

The Kubernetes nodes and Cilium networking were already healthy, but the environment still behaved like a collection of manually administered systems. The objective of this phase was to establish a repeatable deployment path where application configuration lived in Git, Argo CD reconciled the intended state, Helm supplied reusable packaging, and every service appeared in a central health and synchronization view.

A root application now drives the rest of the lab platform.

flowchart LR
  GIT[Private GitHub Repository] --> ROOT[Argo CD Root Application]
  ROOT --> NS[Namespace Application]
  ROOT --> STORAGE[Local Path Provisioner]
  ROOT --> KUMA[Uptime Kuma]
  STORAGE --> PVC[Persistent Volume Claim]
  PVC --> KUMA
  KUMA --> DISCORD[Discord Lab Alerts]
        

Git as the source of truth

The private GitOps repository stores bootstrap objects, child applications, namespace definitions, chart versions, and application values.

App-of-apps structure

The root Argo CD application watches the applications directory and creates or reconciles each child application.

Separated access

Argo CD uses read-only repository access while the administration path uses a separate write-enabled deploy key.

Central operational view

Application health, synchronization status, source, chart revision, namespace, and destination are visible from one dashboard.

Argo CD dashboard showing the root application, Local Path Provisioner, and Uptime Kuma healthy and synchronized
Figure 1. Argo CD application view after the root application reconciled the namespace layer, dynamic storage provisioner, and first operational workload.

Upstream defaults are preserved beside the active configuration.

Each Helm-based service keeps a complete upstream values file as values.defaults.yaml and a separate editable values.yaml. This leaves the supported configuration surface searchable in Git, preserves the original defaults used during deployment, and makes later upgrades or troubleshooting easier to reason about.

Repository pattern

infrastructure/
├── local-path-provisioner/
│   ├── values.defaults.yaml
│   └── values.yaml
└── uptime-kuma/
    ├── values.defaults.yaml
    └── values.yaml

The first stateful workload required a default StorageClass.

The cluster initially returned no StorageClass resources. Rancher Local Path Provisioner was deployed through Argo CD and configured as the default storage class. It provides functional dynamic provisioning for the current lab phase without introducing the operational weight of a distributed storage system before it is needed.

Why local-path now

The immediate requirement is reliable functionality, repeatable deployment, and persistent application state. Local Path Provisioner meets that requirement with minimal infrastructure overhead.

Known limitation

The storage is node-local and not replicated. It is suitable for this learning phase, but it is not the final answer for workload mobility or high availability.

Provisioning behavior

The default StorageClass uses delayed volume binding so storage is created after Kubernetes selects the workload node.

Future state

Ceph or another distributed storage layer can replace this implementation when resilience becomes a primary platform requirement.

Terminal validation showing the default local-path StorageClass, healthy provisioner pod, and Uptime Kuma persistent volume claim bound at two gibibytes
Figure 2. Dynamic storage validation showing the default Local Path StorageClass, a healthy provisioner pod, and Uptime Kuma's bound 2 Gi persistent volume claim.

Uptime Kuma validated the complete Git-to-running-service workflow.

Uptime Kuma was selected because it immediately adds operational value while exercising the full platform path. The application was deployed from a version-pinned OCI Helm chart, configured through the private Git repository, given a dedicated namespace, and attached to a 2 Gi persistent volume. SQLite was selected because the deployment is small and does not justify a separately managed database service.

Validated deployment path

Git commit
    ↓
Private GitHub repository
    ↓
Argo CD root application
    ↓
Uptime Kuma child application
    ↓
Pinned Helm chart + tracked values
    ↓
Kubernetes Deployment, Service, and PVC
    ↓
Running monitor with persistent state

The monitoring service now reports failures into the lab Discord.

A dedicated Discord webhook connects Uptime Kuma to the lab alert channel. Initial testing exposed two configuration failures: the notification was first set to post to a thread without a valid thread identifier, and a later test used an invalid webhook token. Separating the channel mode problem from the credential problem led to a clean configuration using a normal channel webhook with no channel or thread ID required.

Failure one: thread mode

The request included an undefined thread identifier because the Discord notification mode did not match the target channel type.

Failure two: invalid token

Discord returned an unauthorized response until the webhook was recreated and the complete URL was copied into Uptime Kuma.

Discord lab alerts channel showing a successful Uptime Kuma test notification from the Lab Monitor application
Figure 3. Successful Uptime Kuma webhook test in the dedicated lab operations alert channel.

The home lab now has the beginnings of a real platform control plane.

This phase changed the cluster from a working Kubernetes installation into an environment with a defined operating model. Application state is visible in Argo CD, configuration is versioned in Git, Helm releases are pinned, persistent storage is provisioned automatically, service state survives container replacement, and monitoring events can leave the cluster through an operational notification path.

Established

  • Git-backed desired state
  • Argo CD app-of-apps management
  • Version-pinned OCI Helm deployments
  • Dynamic persistent storage
  • First stateful operational service
  • Discord outage and recovery path

Next platform layers

  • Ingress and internal service naming
  • TLS and certificate management
  • Secrets management
  • Metrics and logging
  • Distributed storage
  • Backup and recovery validation

This entry extends the Kubernetes bootstrap journal.