title: Set up local package mirrors
Set up local package mirrors
You'll finish this page with one host in your fleet mirroring an upstream package archive (apt, dnf, apk, zypper, pacman, or Homebrew bottles) and the rest of the fleet pulling packages from that host over HTTP instead of from the public internet.
When you'd use this: the fleet pulls the same packages over and over; the bandwidth bill or the bootstrap-from-scratch time is real; some envoys are on slow / metered / firewalled links and can only reach internal hosts; you want to pin a known-good snapshot of a distro's archive while production-tested updates happen on a schedule.
When you'd skip this: the fleet is small enough that each envoy fetching from upstream isn't a problem, or you already have an enterprise package proxy (Artifactory, Pulp, Spacewalk) that handles this story.
How it fits
Each <pm>-mirror configcrate is a self-contained stacks recipe: install the upstream mirror tool, write its config, run it on cron, serve the resulting tree over HTTP via nginx. The same shape repeats across package managers — only the tooling, the upstream URL, and the on-disk layout change.
Not curator. Not the swarm. One host runs the upstream mirror; the rest of the fleet points its package manager at that host's nginx. That's the whole architecture. The reason to use a stacks configcrate here rather than curator's P2P substrate is that when one host already has the bytes from upstream, there's no reason to also P2P-distribute them across the fleet — the bandwidth-save story is "fetch once from the internet, serve N times on the LAN."
Pair the mirror configcrate with the nginx configcrate on the same host. The mirror configcrate drops a server block into /etc/nginx/conf.d/ and notifies the nginx service for reload — it does not install nginx itself.
Shipped recipes
Each example lives at example-configs/stacks/configcrates/system/<pm>-mirror.vgo.example. Copy into your stacks, set vars to match your upstream of choice, publish.
| Configcrate | OS family | Tooling | Upstream | Client-side config |
|---|---|---|---|---|
apt-mirror |
Debian / Ubuntu | apt-mirror (package) + cron + nginx |
archive.ubuntu.com/ubuntu (or any Debian archive) |
deb http://<mirror>/<upstream-host>/<path> <distribution> <components> in /etc/apt/sources.list |
dnf-mirror |
RHEL / Fedora / Rocky / AlmaLinux / Amazon Linux | reposync (dnf-utils) + createrepo_c + cron + nginx |
configured dnf source on the mirror host |
baseurl=http://<mirror>/<repoid>/ in a .repo file |
apk-mirror |
Alpine | rsync from rsync://rsync.alpinelinux.org/alpine/ + nginx |
Alpine official | http://<mirror>/alpine/<version>/main in /etc/apk/repositories |
zypper-mirror |
openSUSE / SLES | dnf reposync + createrepo_c (rpm-md, same shape as dnf) |
openSUSE / SLES mirror | baseurl=http://<mirror>/<repoid>/ in /etc/zypp/repos.d/<repo>.repo |
pacman-mirror |
Arch Linux | rsync from a tier-1 Arch mirror + nginx |
configured tier-1 mirror | Server = http://<mirror>/archlinux/$repo/os/$arch in /etc/pacman.d/mirrorlist |
brew-bottle-mirror |
macOS | nginx proxy_cache fronting ghcr.io/v2/homebrew/core/ |
GitHub Container Registry (Homebrew's bottle store) | export HOMEBREW_BOTTLE_DOMAIN=http://<mirror> on each client |
Different shape: brew-bottle-mirror
Five of the six examples follow the same pattern — rsync (or rsync-like) from upstream, then serve the resulting directory tree from disk. brew-bottle-mirror is the odd one out: Homebrew bottles live on GitHub Container Registry as OCI blobs, not on a traditional rsync-able mirror tree. The pragmatic shape on macOS is a transparent caching reverse proxy — nginx with proxy_cache_path fronting ghcr.io. First fetch goes to GitHub, every subsequent fetch is served from local disk. Clients set HOMEBREW_BOTTLE_DOMAIN to the mirror host and brew install does the rest.
The trade-offs: no offline-from-day-one (the first miss must reach upstream), but no preflight rsync either (the cache fills in response to demand). For a typical macOS-laptop fleet this is the operationally-correct shape.
Deferred recipes — pattern is documented, the example isn't shipped
These ecosystems are workable in principle but aren't included in the current shipping set. Each entry lists what tooling you'd use if you wrote the configcrate yourself; the pattern is identical to the shipped examples (install tool, cron, nginx).
Windows
- Chocolatey — NuGet feeds. Internal-repo story uses a NuGet server (Chocolatey Server / ProGet / static
.nupkgdirectory). The--internalizeflag that letschoco downloadrewrite community packages for internal serving is a Chocolatey Pro feature. Free path: serve a static.nupkgdirectory via IIS or nginx +choco pushpackages you've manually downloaded. Deferred per Vigo's Linux-first cadence — no Windows new-feature work until a customer with a real Windows fleet asks. - Scoop — git-based buckets. "Mirroring" means a local git mirror of the bucket repo plus an HTTP server in front of it. Smaller-userbase tool; deferred for the same reason.
- Winget — REST API + manifest tree. Source-mirror means hosting your own private source. Workable but more involved than the file-tree mirrors above; deferred.
BSD / illumos
- FreeBSD
pkg—pkg fetch -dinto a directory + nginx serving it; clients pointpkg'surlat the mirror. - OpenBSD
pkg_add—rsyncfrom an OpenBSD mirror;pkg_addreadsPKG_PATHURLs. - illumos / Solaris IPS —
pkgrecvinto a local repository,pkg.depotd(or nginx in front of/var/share/pkg/repositories/) to serve. Workable; deferred until lab demand. - MacPorts — rsync from upstream MacPorts; less commonly mirrored.
Language registries
These are a different category — "language package index mirrors" rather than "OS package mirrors" — and the tooling typically runs its own daemon rather than producing a static tree nginx can serve. If you want them, file a separate item:
- PyPI →
devpi-serverorbandersnatch - npm →
verdaccio(full registry) ornpm proxy - RubyGems →
geminaboxorrubygems-mirror - Go modules → Athens (
GOPROXY) - Cargo → custom alternative-registry mirror
Container images
Already covered by curator's Docker Registry v2 shim — see Set up curator. Not a package-mirror configcrate concern.
Operational notes
- One mirror host per family is the v1 shape. A single Vigo-managed mirror host can run several of these configcrates at once (one per family) —
apt-mirrorfor the Ubuntu envoys,dnf-mirrorfor the Rocky envoys,apk-mirrorfor the Alpine envoys, etc. They serve under non-conflictinglocationblocks of the same nginx instance and disk paths. - Disk usage is significant. A full mirror of Ubuntu main + restricted + universe + multiverse for one release runs around 250–400 GiB. Plan disk before publishing. The
apt-mirror'scleandirective will trim deleted upstream packages, but the working set is the working set. - Cron schedule defaults to nightly in each example (
0 4 * * *). On a slow uplink, stretch this to weekly — fleet convergence isn't blocked on the mirror being fresh, only on it being up. - Sources of truth. If the mirror falls behind (cron failed, disk full, upstream changed), client envoys can still upgrade — apt/dnf/apk will retry from the upstream URLs in the source list. Add the mirror as the FIRST source and keep the upstream as a fallback. The examples set this up.
- First-run convergence. The initial rsync/reposync takes hours and Vigo's per-resource convergence timeout (default 300s for
exec/package/repositoryresources, peragent/src/runner/mod.rs) will kill an inline sync. The examples deliberately set up cron and let the first sync happen out-of-band — operator runs it once by hand or waits for the first cron tick. - Retraction. Each configcrate has a paired retract file at
example-configs/retractions/configcrates/system/<pm>-mirror-retract.vgo.example. Retract removes the cron entry, the nginx site, and (where applicable) the mirror tool package, but leaves the mirrored tree on disk — operators delete that manually after confirming the fleet has flipped back to upstream. The disk delete isn't reversible and shouldn't be a convergence-cycle side-effect.
When something goes wrong
| Symptom | Likely cause | Where to look |
|---|---|---|
| Mirror tree empty or stale | First cron tick hasn't run yet, or cron failed | journalctl -u cron, the mirror tool's log under /var/log/<pm>-mirror/ |
| Clients get 404s on package metadata | nginx location block wrong, or upstream archive layout changed |
/var/log/nginx/<pm>-mirror.access.log for the actual paths requested |
| Disk full | The working set is bigger than expected; clean on apt-mirror |
df -h /var/spool/<pm>-mirror; trim the components list if you don't need all of them |
| Clients fall back to upstream on every install | Mirror host unreachable, certificate name mismatch, or sources.list points at the upstream first | apt-cache policy <pkg> (or equivalent) to confirm which source is being preferred |