How to build a public Kubernetes GitOps repo that others can reuse — without leaking secrets or creating a maintenance nightmare.

The Problem You May Hit#
You’ve been running GitOps and Kubernetes in your homelab for a while. Things are working well. You want to share your configuration publicly so others can replicate it. But you also need to keep your actual cluster configuration private—real hostnames, IP addresses, secrets, all the stuff you definitely don’t want on a published Git repository.
At this point, you may ask the question:
How do I sync my public GitOps repo into my private one?
Actually, the correct solution isn’t syncing repositories. It’s consuming multiple repositories directly in a GitOps operator. Let me show you what I mean.
Repositories Have Different Purposes#
Here’s the mental model that works for me:
| Repository | Responsibility |
|---|---|
| Public repo | Reusable Kubernetes baseline |
| Private repo | Cluster-specific desired state |
Trying to “sync” them leads to problems—accidental secret leakage, merge conflicts, drift between repositories, and unclear ownership of configuration.
Instead, treat your public repository like upstream software. Think about how you use Helm charts: the chart defines what can be deployed, and your values.yaml file defines how it’s deployed in your specific environment.
Public repo = dependency
Private repo = configuration
This is exactly like Helm charts and values.yaml, just applied to your entire infrastructure.
Repository 1: Public (Reusable Baseline)#
My public repository contains no secrets, no IP addresses, no real hostnames—nothing cluster-specific at all.
Instead, it defines what to deploy. Base configurations. Default values. Generic Helm charts. Kustomize bases. Everything structured so that someone else could take it and adapt it to their own infrastructure.
Repository 2: Private (Cluster Overlay)#
My private repository contains all my Argo CD Applications, environment-specific overrides, and values that are specific to my actual cluster.
This is where I define how and where things run in my infrastructure.
Here’s the key insight: Argo CD consumes both repositories directly. No syncing. No copying files. No git submodules or clever automation. Argo CD just reads from both repos at the same time.
What Goes in the Public Repo#
The public repository needs to be something someone else can actually use. Here is an example structure:
homelabs/
├── apps/
│ ├── app-01/
│ | ├── deployment.yaml
│ | ├── kustomization.yaml
│ | ├── pvc.yaml
│ | ├── secret.yaml.template
│ | └── service.yaml
│ └── app-02/What Belongs Here#
Generic stuff that makes sense on any cluster: Helm charts, Kustomize bases, default values, placeholder domains. These are reusable building blocks.
What Doesn’t Belong Here#
Anything specific to my cluster: secrets, IP addresses, real hostnames, storage paths. If it wouldn’t make sense on someone else’s cluster, it doesn’t go in the public repo.
Every time I add something to the public repo, I ask: “Could someone else use this as-is?” If the answer is no, it belongs in the private repo instead.
What Goes in the Private Repo#
The private repository represents my actual cluster’s desired state, for example:
homelabs-private/
├── argocd/
│ ├── kustomization.yaml
│ └── projects/
│ └── homelab.yaml
├── clusters/
│ └── home-prod/
│ ├── argocd/
│ │ ├── app-of-apps.yaml
│ │ └── applications/
│ │ ├── app-01.yaml
│ │ └── app-02.yaml
│ └── overlays/
│ ├── app-01/
│ │ ├── kustomization.yaml
│ │ └── secret.yaml
│ └── app-02/
│ ├── kustomization.yaml
│ └── secret.yamlThis is where Argo CD itself lives, where applications are instantiated, where versions are pinned, and where all my cluster-specific configuration exists.
How Argo CD Ties Everything Together#
Argo CD has a feature called multi-source Applications. This is what makes the entire pattern work.
Here’s an actual example from my setup:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-01
namespace: argocd
spec:
project: homelab
sources:
- repoURL: https://github.com/example/homelabs.git
targetRevision: main
path: apps/app-01
- repoURL: https://github.com/example/homelabs-private.git
targetRevision: main
path: clusters/home-prod/overlays/app-01
destination:
server: https://kubernetes.default.svc
namespace: app-01
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=trueLook at what this achieves:
- The public repo defines the application (
homelabs/apps/app-01) - The private repo defines the cluster-specific bits (
homelabs-private/clusters/home-prod/overlays/app-01/secret.yaml) - There’s no copying, no syncing, no risk of secrets leaking
When I update the public repo with a new feature, other people can pick it up by bumping a tag (or by tracking main). When I update my private overlays (secrets, overrides), only my cluster changes. Clean separation of concerns.
How Others Can Reuse My Public Repository#
A new user doesn’t need to clone my entire private structure or figure out what’s specific to my setup.
They just create their own private repository, write their own Argo CD Applications, and reference my public repo as upstream. They pull the base configuration from my public repo and apply their own overrides in their private repo. No complexity. No maintenance burden. Lowest-friction reuse.
Versioning and Promotion: Why I Use Trunk-Based Development#
I used to overthink this. I tried GitFlow with main, develop, feature, release, and hotfix branches. It is not the best for GitOps.
Don’t get me wrong—GitFlow works perfectly well in the right context. At work, I use GitFlow for medical software development where we have rigorous testing and release processes, with strict regulatory requirements. In that environment, the ceremony of GitFlow makes sense. I also use GitOps at work for the platform side; for GitOps—there and here—I want something simpler: determinism, immutability, and clear promotion paths.
What I Actually Use Here#
Trunk-based development:
main → always releasable
feature/* → short-lived
tags → v1.0.0, v1.1.0Public Repo Versioning#
The main branch is always stable. Every meaningful change gets a new tag using semantic versioning. No environment branches. No long-lived feature branches.
Private Repo Versioning#
The main branch reflects my current cluster state. Promotion happens through version bumps, not branch merges.
If I had multiple environments (dev/staging/prod), they’d be directories, not branches:
clusters/
├── home-dev/
└── home-prod/How I Promote a New Version#
This is the actual workflow I use:
1. Tag a new version in the public repo:
cd homelabs/
git tag v1.2.0
git push origin v1.2.02. Update the private repo to reference the new version:
cd homelabs-private/
# Edit the Application manifest
vim clusters/home-prod/argocd/applications/app-01.yaml
# If you pin the public repo to tags, bump the homelabs source targetRevision: v1.1.0 → v1.2.0
git add clusters/home-prod/argocd/applications/app-01.yaml
git commit -m "Promote app-01 to v1.2.0"
git push origin main3. Argo CD automatically syncs the new version to my cluster (or I manually sync if auto-sync is disabled).
If I had a dev environment, I’d test there first:
# Update dev environment
vim clusters/home-dev/argocd/applications/app-01.yaml
# Change to v1.2.0, test it, then promote to prodThis workflow is clean, auditable, and requires zero automation beyond Argo CD itself.
The Mental Model and Long-Term Benefits#
Don’t sync repositories—consume them. The public repo holds reusable baselines; the private repo holds my cluster’s desired state. Argo CD multi-source Applications exist for exactly that split.
After running this for a while, the practical wins are straightforward: it’s obvious where a change belongs, Git stays aligned with what’s running, rollbacks are a version pin in the private repo, and I can publish the public tree without worrying about hostnames or secrets leaking. The same layout scales to multiple clusters or stricter audit requirements; it works fine at homelab scale too.
