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.

Repositories have different purposes#
| Repository | Responsibility |
|---|---|
| Public repo | Reusable Kubernetes baseline |
| Private repo | Cluster-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: falsePrivate (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.comArgo 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=trueThe 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.0The 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.0Bump 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 mainArgo 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.
