Dual-Push, Pull Mirrors, and Forgejo's Ground Rules
My per-commit dual-push ritual turned out more honest than a pull mirror that would quietly drop a 631 MB branch GitHub never had.
I opened the terminal and typed the ritual I had made automatic: git push origin main && git push forgejo main. Two hosts, one repo, every commit. The ritual ran smoothly until I read the advice that keeps circulating: if GitHub stays your primary host, just set up a pull mirror on Forgejo and let it handle syncing. I almost followed it. For most cases that advice genuinely makes sense, and the official documentation is clear about it [1]. One thing stopped me: the backup/pre-rewrite-20260911 branch, 631 MB, living on my Forgejo had never existed on GitHub. A pull mirror follows upstream exactly, so the very pattern "everyone recommends" was the one that would quietly throw that branch away.
How pull mirrors actually behave
On Forgejo you can only create a pull mirror at repository creation time: through New Migration with the "This repository will be a mirror" box checked. An existing regular repo cannot be converted into a mirror, and a mirror repo is read-only [1]. Syncing runs periodically, 8 hours by default with a 10 minute minimum [5], plus a Synchronize Now button for the impatient [1].
That "follow upstream exactly" property cuts both ways. For a repo whose contents match GitHub one to one, it is a light, disciplined backup. But the mirror has no idea which local branches are precious. Whatever is missing upstream disappears from the mirror's view. A superset cannot travel this road.
Push mirrors and their limits
The other direction, a push mirror from Forgejo to GitHub, carries an official warning: it always force-pushes and overwrites the remote [1]. Without a Branch Filter, Forgejo runs git push --mirror, sending every branch [1]. On GitHub's side, the push policy feature (still public preview) can limit how many branches and tags change in a single push, and it blocks git push --mirror outright [2]. Put the two together and an unfiltered push mirror can fail completely on a repo with a strict policy.
There is another gap: LFS. LFS over an SSH push mirror is not implemented in Forgejo, so LFS objects are not mirrored [1]. A repo leaning on LFS plus an SSH mirror can feel safe while its large files never actually move.
When dual-push is the right call
So I stopped hunting for a universally "best" pattern. For a personal repo holding giant branches, manual dual-push is the more honest option: I decide what goes where, and no automated process silently subtracts data. The habit does demand discipline; a failed push has to be repeated before the next commit, otherwise the two hosts slowly drift apart.
This two-forge life is not exotic. The Dutch government runs code.overheid.nl as a self-hosted Forgejo on SSC-ICT infrastructure, soft-launched April 24, 2026 and still a gradual pilot; they ruled GitHub out as proprietary [3]. Codeberg hosts around 678,000 projects as of August 2026, roughly double compared to the previous November, with no paid plan or enterprise tier [4]. The ecosystem is mature enough to trust with a second copy of your code.
My rule is simple now. Repos that fit GitHub and match it: pull mirror, light and hands-off. Superset repos with branches GitHub can never accept: disciplined dual-push. The "smart" automation loses to two manual keystrokes that tell the truth about what gets pushed where.
Sources
- Forgejo Docs, Repository Mirrors (accessed September 12, 2026)
- GitHub Docs, Managing the push policy for your repository (accessed September 12, 2026)
- It's FOSS, The Netherlands is Quietly Building Its Own GitHub Replacement (accessed September 12, 2026)
- Code To Cloud, Codeberg: The Non-Profit GitHub Alternative (accessed September 12, 2026)
- Forgejo Docs, Configuration Cheat Sheet (accessed September 12, 2026)