Open-source product. The README says "deploy in 10 minutes". Someone clones it, runs it, gets a 502 — because the maintainers run nginx with a specific proxy_pass in front and forgot to mention it.
Or worse: an API key ended up in a public commit because the prod config lived in the same repo.
Same bug, different shape: one repo trying to do two different jobs.
An examples/ folder doesn't help
The first instinct is always the same: "let's separate the example from the real thing." So examples/ folders appear. docs/deployment-example/. Notes in the README.
Doesn't work. Within a month the example drifts from reality. Nobody updates examples/nginx.conf when they're fixing the real config at 3am. A year later the example is broken and new users get 502s again.
Not because maintainers are lazy. Two things with different lifecycles just can't coexist cleanly in one place.
These are two different things
The deployment template that users copy — that's part of the product. It's generic, with {{ DOMAIN }} and ${PORT} instead of real values. It changes when the product changes. Contributors see it, CI validates it. It's public.
Your actual deployment is yours. Real domains, certificates, IPs, secrets. Changes when you need it to. Nobody else needs to know it exists. It's private.
Mixing them in one repo is like keeping your production database and a SQL tutorial in the same schema. You'll break either the lesson or the prod.
Three approaches that don't work
.gitignore on the private files. One typo and a secret is in public git history forever. git clean -fdx wipes the infra. A landmine you set under yourself.
Submodule pointing at a private repo. Leaks the private repo's existence through .gitmodules. Contributors can't clone. Submodules are also a maintenance burden: pinning, drift.
"We'll document it." Drifts from reality within 2–3 deploys. Nobody opens a docs PR at 3am.
What works
Two separate repos. No code-level coupling.
Public — the product's monorepo. Has templates/nginx/, templates/deploy/ with placeholders and a README. Everyone sees it, it goes through review, it versions with the product.
Private — your actual infra. Real domains, certificates, secrets, CI/CD for your VPS. No reference to it from the public repo — users don't need to know it exists.
The only link is in the private repo's docs: "deploys github.com/<org>/<product>, based on templates/nginx/". The public repo knows nothing about the private one.
When it pays off
This setup costs more up front: two places to commit, two workflows. It pays off in three situations.
Open source. If the code is public — it's not "if something leaks", it's "when".
The first customer. When someone starts deploying your product themselves, they need a clean template without your specific domains baked in. Otherwise they copy your kblabs.ru.conf and spend time figuring out why it failed.
The team grows. You can't give a junior SSH to prod, but you can give them a PR to templates/nginx/. The separation makes delegation safe.
When I set up nginx for kblabs.ru, I was tempted to put it all under infra/ in the monorepo. Didn't. Now we have templates/nginx/ (public, generic) and kb-labs-infra (private, with the real secrets). When the first customer tries to self-host — they'll have something clean to copy, without having to decode my specific config.
One repo shouldn't do two jobs.