Reusing a Git mirror on self-hosted runners Jump to heading
On a repository with a decade of history, a full clone can take longer than the build. Every job pays it, and the transfer is identical each time β the same objects, from the same server, to machines that already had them an hour ago. On hosted runners there is nothing to do about that beyond trimming what you fetch. On machines you own, a local bare mirror turns the fetch into a disk copy. This recipe sets one up safely, extending CI caching and runner performance.
When to use this approach Jump to heading
- You run self-hosted runners, or runners on long-lived virtual machines.
- The repository is large enough that fetch time is a visible share of the pipeline.
- Several jobs on the same host clone the same repository.
- Jobs need more history than a shallow clone provides.
- If your runners are ephemeral containers on hosted infrastructure, there is no persistent disk to hold a mirror; use fetch trimming instead.
Step 1 β Create the mirror Jump to heading
A mirror is a bare clone that tracks every ref, which is what makes it usable as a reference for any branch a job might check out.
sudo install -d -o runner -g runner /srv/git
git clone --mirror https://github.com/acme/app.git /srv/git/app.git
# Size it, so you know what you are committing to on each runner host
du -sh /srv/git/app.git # Verification: the mirror knows every branch and tag
git --git-dir=/srv/git/app.git for-each-ref --count=5 'refs/heads/*'
git --git-dir=/srv/git/app.git rev-parse --is-bare-repository Step 2 β Clone from it, then dissociate Jump to heading
--reference tells Git to borrow objects from the mirror rather than transferring them. --dissociate then copies the borrowed objects into the new clone, so the result is a normal, self-contained repository.
git clone --reference /srv/git/app.git --dissociate \
"https://github.com/acme/app.git" "$WORKDIR" # Verification: no alternates file remains, so nothing depends on the mirror
test -f "$WORKDIR/.git/objects/info/alternates" \
&& echo "STILL BORROWING β dissociate did not run" \
|| echo "self-contained clone"
git -C "$WORKDIR" fsck --connectivity-only --no-progress SAFETY WARNING β omitting
--dissociateleaves the clone borrowing objects from the mirror through an alternates file. If the mirror is then repacked, pruned or deleted while a job is running, the working copy loses objects mid-build and produces corruption errors that look like a hardware fault. Always dissociate for disposable job clones; reserve borrowing for long-lived developer clones where somebody is watching.
Step 3 β Refresh the mirror on a timer, not per job Jump to heading
Refreshing inside the job serialises every job behind a network fetch and reintroduces the cost you were removing.
# /etc/systemd/system/git-mirror.service
[Unit]
Description=Refresh the local Git mirror
[Service]
Type=oneshot
User=runner
ExecStart=/usr/bin/git --git-dir=/srv/git/app.git remote update --prune # /etc/systemd/system/git-mirror.timer
[Unit]
Description=Refresh the local Git mirror every ten minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
[Install]
WantedBy=timers.target # Verification: the mirror is current and the timer is firing
systemctl enable --now git-mirror.timer
git --git-dir=/srv/git/app.git log -1 --format='%ci %h' origin/main 2>/dev/null \
|| git --git-dir=/srv/git/app.git log -1 --format='%ci %h' main Step 4 β Handle the stale-mirror case Jump to heading
A mirror refreshed every ten minutes will sometimes lack the commit a job needs, because the push that triggered the job is newer than the last refresh. The clone must still succeed.
# Clone from the mirror, then fetch only what is missing from the real remote
git clone --reference /srv/git/app.git --dissociate "$REPO_URL" "$WORKDIR"
git -C "$WORKDIR" fetch --no-tags origin "$GIT_SHA" || git -C "$WORKDIR" fetch origin
git -C "$WORKDIR" checkout --detach "$GIT_SHA" # Verification: the commit under test is present after the fetch
git -C "$WORKDIR" cat-file -e "$GIT_SHA^{commit}" && echo "commit present" Because the mirror supplies almost everything, the top-up fetch transfers only the handful of new objects β typically a fraction of a second even on a large repository.
Step 5 β Keep the mirror healthy Jump to heading
A mirror that is never maintained grows unboundedly and eventually fetches slower than the network.
# Repack periodically; a mirror benefits from aggressive settings a working copy does not
git --git-dir=/srv/git/app.git gc --aggressive --prune=now
# Or let the scheduler handle it
git --git-dir=/srv/git/app.git maintenance start
git --git-dir=/srv/git/app.git config maintenance.gc.enabled true # Verification: loose object count should stay low after maintenance
git --git-dir=/srv/git/app.git count-objects -vH | grep -E 'count|size-pack' The scheduling options are covered in scheduling git maintenance for background repacking, and apply as much to a mirror as to a developerβs clone.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Can several runners share one mirror over a network filesystem? Jump to heading
They can, and it frequently performs worse than the network clone it replaced, because Gitβs access pattern over a shared filesystem is latency-bound. Keep a mirror per host; the disk is cheaper than the debugging.
What happens if the mirror is corrupted? Jump to heading
Jobs that have already dissociated are unaffected. New clones fail at the reference step, which is loud rather than subtle. Guard it by running git fsck from the refresh timer occasionally and recreating the mirror on failure β recreating is a single command and takes one clone.
Does this work for private repositories? Jump to heading
Yes, provided the mirror is refreshed with credentials the runner user holds and the disk is protected accordingly. Remember that the mirror contains the entire repository in plain form on disk, so the hostβs security posture is now part of your source-code protection.
Related Jump to heading
- CI Caching & Runner Performance β the parent topic and where fetch sits in the phase breakdown.
- Shallow Clone vs Full History in CI β the alternative when runners are ephemeral.
- Scheduling Git Maintenance for Background Repacking β keeping the mirror fast.