July 17, 2026
One Command to Rule Them All
Every time I rebuild my home lab OpenShift cluster, I want to go from a fresh install to a fully configured platform with a single command:
oc apply -k cluster-configs/
That’s it. Operators install, ArgoCD bootstraps itself, platform services come up, observability starts collecting, and the cluster reaches its desired state — all reconciled by GitOps. When something drifts, ArgoCD puts it back.
This post walks through how I structured the openshift-bootstrap-gitops repo to make that work, and the lessons learned from actually running it on a single-node OpenShift cluster called “tallgeese.”
What Gets Deployed
The bootstrap manages a broad stack:
Cluster infrastructure:
- LVM Storage + internal image registry
- OpenShift GitOps (ArgoCD)
- Security: htpasswd OAuth, admin RBAC
- Node maintenance: catalog pod pruning, journald retention, kubelet log rotation, CRI-O image pruning, system-reserved resources
Platform services:
- Advanced Cluster Management (ACM)
- Advanced Cluster Security (ACS/StackRox)
- Red Hat Developer Hub (Backstage)
- Keycloak (RHBK)
- Tekton Pipelines
- Dev Spaces
- Serverless (including SonataFlow)
External repos (referenced by ArgoCD Applications):
- Cluster observability: Loki, Tempo, OpenTelemetry, MinIO
- Network observability
- External Secrets Operator
- Red Hat OpenShift AI
- Service Mesh 3
- KEDA autoscaling
The Two-Layer Structure
The repo uses two directories that work together:
cluster-configs/ — Kustomize manifests for every component. Each component gets its own subdirectory with its own kustomization.yaml:
cluster-configs/
├── kustomization.yaml # Root: aggregates everything
├── storage/ # LVM operator + image registry
├── acs/ # StackRox
├── developer-hub/ # Backstage
├── acm/ # Advanced Cluster Management
├── observability/ # Loki, Tempo, OTel, MinIO
└── ...
applications/ — One ArgoCD Application manifest per component, also aggregated by a root kustomization. Each Application points back at either a path in this repo or an external repo.
The root kustomization applies common labels including SkipDryRunOnMissingResource — this is essential because many manifests reference CRDs that don’t exist yet (they arrive when the operator installs). Without this annotation, oc apply would fail on the first run.
Base/Overlay for Shared Infrastructure
The observability stack is a good example of a pattern that emerged from practical necessity. MinIO, Loki, and Tempo are deployed once by the observability Application. Other components (network observability, service mesh) need to write to these backends.
Rather than duplicating the backend configs, each consumer repo has two overlays:
integrated/— References the shared MinIO/Loki/Tempo instances deployed by the observability appstandalone/— Deploys its own backends for use outside the home lab
This means the observability app must sync before its consumers. ArgoCD’s sync waves and health checks handle the ordering — the consumer Applications wait until the backends report healthy.
The Secrets Problem
Secrets are deliberately excluded from GitOps. Every component that needs secrets has a secrets/ directory with a README explaining what to create manually:
secrets/
├── README.md # "Create these secrets before syncing"
├── .gitignore # Keeps secrets out of git
└── example.yaml # Template to copy and fill in
The plan is to eventually adopt External Secrets Operator to pull credentials from a vault. For a home lab, the manual approach works — but it means the bootstrap isn’t truly single-command. There’s always that oc apply -f secrets/ step first.
Things I Learned the Hard Way
SNO disk partitioning is install-time only. MachineConfigs for disk layout must be in the install manifests. You can’t partition disks via GitOps post-install. I learned this after rebuilding twice.
CRI-O image pruning matters on single-node. With limited disk, you need aggressive image garbage collection. A MachineConfig sets imageMinimumGCAge and imageGCHighThresholdPercent to keep the node from filling up.
ArgoCD bootstrapping ArgoCD is circular. The OpenShift GitOps operator installs a default ArgoCD instance. The bootstrap creates a customized one. On first apply, the operator isn’t installed yet, so the ArgoCD CR fails. The fix: the operator subscription is a resource in the same kustomization, and SkipDryRunOnMissingResource lets the CR be applied even though the CRD doesn’t exist yet. The operator installs, the CRD appears, and ArgoCD reconciles itself on the next sync.
System-reserved resources prevent OOM kills. On a single node running everything, the kubelet needs explicit system-reserved values for CPU and memory. Without them, platform pods can starve the kubelet itself.
The Road to GitOps
If you’re earlier in your GitOps journey, I also have a road-to-gitops repo that walks through the progression: imperative commands → declarative manifests → Kustomize base/overlay → Helm packaging → Helm + Kustomize → ArgoCD Applications → App-of-Apps → ApplicationSets. It’s a presentation with nine progressive demos, ending with a full PR-to-production flow with rollback.
The bootstrap repo is the end state of that journey. It’s not where you start — but it’s where you end up when you’ve been managing a cluster long enough to know what breaks.
What’s Next
The biggest open item is wiring up a real developer experience: Tekton CI pipelines triggered by webhooks, Quarkus software templates in Developer Hub, and Dev Spaces for cloud-native development environments. The pieces are all installed — the integration is the hard part.
The full repo is at ultraJeff/openshift-bootstrap-gitops.
