Skip to main content

A Pattern for Building Reproducible Public Kubernetes GitOps Repos Without Exposing Sensitive Data

Minh Phuong Nguyen
Author
Minh Phuong Nguyen

I’ve been running GitOps in the homelab long enough that I wanted to publish the reusable bits. The cluster itself still has hostnames, addresses, etc. that should never land on a public GitHub repo.

The tempting question is: how do I sync a public GitOps repo into a private one? I don’t. Argo CD reads both repositories as sources of the same Application.

Image

Repositories have different purposes
#

RepositoryResponsibility
Public repoReusable Kubernetes baseline
Private repoCluster-specific desired state

Trying to sync them is how histories fight and it is hard to see which repo owns a change. Treat the public repo like upstream software, the same way you already treat a Helm chart: the chart and public values describe what can be deployed, and a private values.yaml describes how this cluster runs it.

Public repo = dependency. Private repo = configuration.

Public (reusable baseline)
#

Nothing cluster-specific belongs here: no IP addresses, no real hostnames, etc. Default Helm values, placeholder domains, generic settings someone else could fork. Every file has to pass “could another cluster use this as-is?” If not, it goes in the private repo.

homelabs/
├── app-01/
│   └── values.yaml
└── app-02/
    └── values.yaml
# homelabs/app-01/values.yaml
replicaCount: 1
image:
  tag: "1.4.0"
ingress:
  enabled: false

Private (cluster overlay)
#

This repo holds Argo CD Applications, version pins, and values that only make sense on my cluster.

homelabs-private/
├── argocd/
│   └── projects/
│       └── homelab.yaml
└── clusters/
    └── home-dev/
        ├── argocd/
        │   ├── app-of-apps.yaml
        │   └── applications/
        │       ├── app-01.yaml
        │       └── app-02.yaml
        └── overlays/
            ├── app-01/
            │   └── values.yaml
            └── app-02/
                └── values.yaml
# clusters/home-dev/overlays/app-01/values.yaml
ingress:
  enabled: true
  hosts:
    - host: app-01.example.com

Argo CD consumes both repositories directly. No copying files, no git submodules, no sync job. Secrets stay out of git (Vault and External Secrets); the private overlay is values, not Secret YAML.

How Argo CD ties everything together
#

A multi-source Application can install an upstream Helm chart and read value files from other git repos in the same spec. The sources with ref: exist so Helm can resolve $homelabs/... and $private/.... They do not render extra manifests.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-01
  namespace: argocd
spec:
  project: homelab
  sources:
    - repoURL: https://charts.example.com
      chart: example-app
      targetRevision: 1.4.2
      helm:
        valueFiles:
          - $homelabs/app-01/values.yaml
          - $private/clusters/home-dev/overlays/app-01/values.yaml
    - repoURL: https://github.com/example/homelabs.git
      targetRevision: v1.2.0
      ref: homelabs
    - repoURL: https://github.com/example/homelabs-private.git
      targetRevision: main
      ref: private
  destination:
    server: https://kubernetes.default.svc
    namespace: app-01
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

The chart targetRevision is a Helm version, not a git branch. The public git source is a tag, so other people consume a release rather than a moving main.

Helm merges the value files in order (public defaults, then the private overlay). When I tag a new public baseline, other clusters pick it up by bumping targetRevision. When I change a private overlay, only this cluster moves.

How others can reuse the public repository
#

They do not need my private tree. They create their own private repo, write Applications that point at the upstream chart plus my public values, and keep hostnames and cluster-specific settings in their overlay.

Versioning: trunk-based, tags for promotion
#

I used to overthink this. GitFlow with main, develop, feature, release, and hotfix is what I use at work for medical software, where the ceremony matches the regulatory process. For GitOps, here and on the platform side, I want a pin I can bump: determinism, immutability, a short promotion path.

Trunk-based development:

main        → always releasable
feature/*   → short-lived
tags        → v1.0.0, v1.1.0

The public main branch stays stable. Meaningful changes get a semver tag. No environment branches.

The private main branch is current cluster state. Promotion is a version bump in the Application, not a merge between long-lived branches. Extra environments are directories, not branches:

clusters/
├── home-dev/
└── home-prod/

How I promote a new version
#

Tag the public repo:

cd homelabs/
git tag v1.2.0
git push origin v1.2.0

Bump the public source in the private Application (and the Helm targetRevision if the chart moved):

cd homelabs-private/
# targetRevision on the homelabs ref: v1.1.0 → v1.2.0
git add clusters/home-dev/argocd/applications/app-01.yaml
git commit -m "Promote app-01 to v1.2.0"
git push origin main

Argo CD syncs the new pin (or I sync it by hand if auto-sync is off).

That is the whole workflow. Consume both repositories; do not sync them. Rollbacks are a pin change in the private Application, and the public tree can stay public because hostnames and addresses never lived there.