Skip to main content

A Pattern for Sharing Replicable Public Kubernetes GitOps Repos Without Leaking Cluster Secrets

Minh Phuong Nguyen
Author
Minh Phuong Nguyen

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

Image

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.

Why GitOps Matters (A Quick Refresher)
#

Before we get into the pattern that works, let’s make sure we’re aligned on what GitOps actually is and why it matters.

GitOps uses Git as the single source of truth for your infrastructure. Every change to your cluster happens through a Git commit. An operator running in your cluster—like Argo CD or Flux—continuously watches your Git repositories and automatically reconciles the cluster state with what Git declares.

In traditional approaches, you push changes to the cluster from outside. You run kubectl apply manually, or your CI/CD pipeline executes deployment commands. The deployment is event-driven. Between deployments, your cluster can drift, and you won’t know unless you apply and check.

With GitOps, all configuration lives in Git. You make changes through pull requests and code reviews. The operator running in your cluster continuously pulls changes every short interval and ensures the cluster state matches Git. No one directly modifies the cluster with kubectl apply. The cluster state always converges toward what Git declares.

Why This Is a Good Idea
#

Version control for everything: Every change has a commit history. You can see who changed what, when, and why. Rolling back is as simple as reverting a commit.

Git is the single source of truth: If your cluster is destroyed, you can recreate it entirely from Git.

Automated reconciliation: The GitOps operator continuously ensures the cluster matches Git. Manual drift gets automatically corrected. Configuration drift becomes impossible.

Better security: Cluster credentials don’t need to be shared with CI/CD pipelines or developers. The operator pulls changes from Git rather than external systems pushing changes to the cluster.

Improved collaboration: Infrastructure changes go through the same review process as code. Pull requests enable discussion and approval workflows.

Disaster recovery: Your entire cluster configuration is in Git. Recovery from catastrophic failure is reproducible and auditable.

Once you experience the confidence that comes from knowing your entire cluster state is version-controlled and continuously reconciled, you wouldn’t want to go back.

The Core Principle: Repositories Have Different Purposes
#

Here’s the mental model that works for me:

RepositoryResponsibility
Public repoReusable Kubernetes baseline
Private repoCluster-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/
├── README.md
├── apps/
│   └── pihole/
│       ├── deployment.yaml
│       ├── kustomization.yaml
│       ├── namespace.yaml
│       ├── pvc.yaml
│       ├── secret.yaml.template
│       └── service.yaml

What Belongs Here
#

Generic stuff that makes sense on any cluster: Helm charts, Kustomize bases, default values, disabled ingress configurations, placeholder domains. These are reusable building blocks.

What Doesn’t Belong Here
#

Anything specific to my cluster: secrets, IP addresses, real hostnames, storage paths, node selectors. If it wouldn’t make sense on someone else’s cluster, it doesn’t go in the public repo.

When a component needs a secret to run I create the real secret.yaml in the private repo overlay.

This discipline is important. 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/
│       │       ├── pihole.yaml
│       │       └── website.yaml
│       └── overlays/
│           ├── pihole/
│           │   ├── kustomization.yaml
│           │   └── secret.yaml
│           └── website/
│               ├── kustomization.yaml
│               └── secret.yaml
└── README.md

This 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: pihole
  namespace: argocd
spec:
  project: homelab
  sources:
    - repoURL: https://github.com/example/homelabs.git
      targetRevision: main
      path: apps/pihole
    - repoURL: https://github.com/example/homelabs-private.git
      targetRevision: main
      path: clusters/home-prod/overlays/pihole
  destination:
    server: https://kubernetes.default.svc
    namespace: pihole
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Look at what this achieves:

  • The public repo defines the application (homelabs/apps/pihole)
  • The private repo defines the cluster-specific bits (homelabs-private/clusters/home-prod/overlays/pihole/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.0

Public 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.0

2. Update the private repo to reference the new version:

cd homelabs-private/
# Edit the Application manifest
vim clusters/home-prod/argocd/applications/pihole.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/pihole.yaml
git commit -m "Promote pihole to v1.2.0"
git push origin main

3. 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/pihole.yaml
# Change to v1.2.0, test it, then promote to prod

This workflow is clean, auditable, and requires zero automation beyond Argo CD itself.

Why This Works: The Mental Model and Long-Term Benefits
#

Here’s the core insight: Don’t sync repositories. Consume them.

Your public repo isn’t “your cluster.” It’s a product that others can consume. Your private repo is your actual cluster configuration. Argo CD is designed to handle this pattern. Multi-source Applications exist specifically for this use case. Once you stop fighting the tool and embrace the pattern, everything becomes simpler.

I’ve been running this setup for a while now, and it scales beautifully. Here’s why:

Clear ownership: The public repo owns the baseline. The private repo owns the environment. No confusion about where something belongs.

No drift: Argo CD continuously reconciles both sources. What’s in Git is what runs in the cluster.

Easy rollback: Change one line in the private repo to pin an older version. Argo CD handles the rest.

Safe sharing: I can share my public repo without worrying about accidentally leaking secrets or cluster-specific details.

Reproducible: If I need to rebuild my cluster from scratch, everything I need is in Git.

This model is also useful in regulated environments where auditability matters. It’s used for multi-cluster setups. And it works just as well for a homelab.