Keeping a Forgejo Mirror in Sync: How Pull Mirroring Actually Works and Where It Bites
Forgejo's repository mirroring is a queue-driven wrapper around native git fetch/push. Here's how pull vs. push mirroring works, a concrete setup walkthrough, and the limitations that bite in practice.
25 Sept 2025, 06:19 UTC

The problem: your code lives somewhere else, but your CI lives here
A common setup: the canonical repository sits on a public host, but your builds, backups, or air-gapped network need a local copy on your own Forgejo instance. You could script git fetch in cron, but then you own the credentials, the scheduling, and the failure alerts. Forgejo's built-in repository mirroring exists precisely to take that toil off your plate — and understanding how it works internally helps you predict where it will fail.
The useful takeaway: Forgejo mirroring is a thin, queue-driven wrapper around native git fetch and git push. It syncs Git data only — commits, branches, tags — and nothing else. If you expect issues or wikis to follow, you will be disappointed; if you expect a reliable Git-level replica, it delivers, provided you size the initial clone correctly.
Pull vs. push: pick the direction that matches your trust boundary
Forgejo supports two directions, and the choice is mostly about who holds the credentials:
- Pull mirroring — your Forgejo instance reaches out to the source and fetches on a schedule. You configure it when creating a repository via New Migration and ticking the mirror option. Forgejo stores the upstream credentials (a personal access token or SSH key, depending on what the source host supports) and refreshes the copy at the interval you set.
- Push mirroring — your Forgejo instance is the source of truth and pushes changes to a remote. You add push mirrors in the repository's settings, one remote per entry, each with its own credentials and interval.
The rule of thumb: mirror from the side you control. If the public host is upstream, pull. If Forgejo is upstream and you want a public read-only copy elsewhere, push.
Why the background queue matters
Forgejo does not run mirror operations inside the web request that created them. Mirror tasks are dispatched to a background worker queue, so a multi-gigabyte fetch doesn't block page loads or time out the HTTP request. This has two practical consequences:
- Creation returns before the clone finishes. The repository page may appear empty or show a sync-in-progress state for minutes on large repos. This is normal, not a bug.
- Syncs are periodic, not instant. The interval you configure is a minimum cadence; actual timing depends on queue load. A commit pushed upstream will appear in the mirror after the next scheduled sync, not immediately.
Because the sync uses Git's native fetch/push machinery, commit hashes, history, and tags are preserved exactly — the mirror is a real Git repository you can clone from, not an export.
A worked example: mirroring a project for local CI
Say you want a local mirror of https://git.example.org/team/service.git on your Forgejo instance at forgejo.internal. This walkthrough assumes Forgejo v1.21 or later; menu labels have shifted slightly across versions, so check your instance's UI.
- In the Forgejo web UI (you need permission to create repositories in the target organization), click + → New Migration.
- Enter the clone URL. For a private source, embed credentials via the Authorization fields — a read-scoped personal access token is the usual choice, since the mirror only needs to read.
- Check This repository will be a mirror. Set the sync interval — the default is typically 8 hours; 15–30 minutes is reasonable for active projects.
- Submit, then watch the repository's Settings → Mirror section (or the repo page's mirror indicator) for the first successful sync timestamp.
To verify end-to-end: push a tagged commit to the source, wait for the interval, then run git ls-remote https://forgejo.internal/team/service.git from any machine with access and confirm the new tag's SHA matches the source. That check is authoritative — it reads the mirror exactly as a client would.
The trade-offs you should plan around
Three limitations matter in practice:
- Git data only. Issues, pull requests, releases metadata, and wikis-as-web-content do not sync. (A wiki's underlying Git repo can be mirrored separately, but the web metadata cannot.) If you need full project portability, that's a migration, not a mirror.
- Initial sync pressure. The first clone of a large repository can hit memory or time limits on a small instance. If the initial mirror fails repeatedly, raise the instance's Git timeout settings or pre-seed the repo and convert it to a mirror.
- Credential lifetime. When the upstream token expires, syncs fail silently from a casual user's perspective — the mirror just goes stale. Make the mirror's last-sync timestamp something you actually monitor; the Mirror settings page shows it, and a stale timestamp is your earliest warning.
What to do next
Set up one pull mirror for a repository you genuinely depend on, shorten the interval to something you'll notice, and add a simple staleness check — even a weekly git ls-remote comparison in CI catches expired tokens before your builds do. Once you trust the cadence, mirroring becomes the boring, reliable plumbing it was designed to be.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.