Upstream cache: bandwidth-limited and air-gapped deployments¶
Repod can act as a transparent pull-through cache in front of public
upstream APT/RPM/APK repositories (Ubuntu, Debian, AlmaLinux, EPEL, Alpine…).
Client machines point their sources.list/.repo files at Repod instead of
the real upstream mirror; the first request for any given file is fetched
and cached, every subsequent request is served locally. This guide covers:
- What the upstream cache is (and isn't)
- Adding a cache target
- Bandwidth-limited sites (cache-on-demand)
- Fully air-gapped sites (seeding via a relay host)
- Verifying the cache is actually working
- Retention, purge, and disk sizing
1. What the upstream cache is (and isn't)¶
The upstream cache is not the same thing as importing packages into your
Repod repository (Import page / mirror scheduled sync). The difference
matters:
| Upstream cache | Import / mirror | |
|---|---|---|
| What's served | Upstream files, byte-for-byte, upstream's own GPG signature | Repod-validated packages, Repod's own GPG signature |
| Validation | None — it's a relay, not a gate | Full pipeline (ClamAV, Grype CVE, dependency check) |
| What a client sees | The real upstream repo, just faster/locally cached | A curated Repod distribution |
| Trigger | Any request from any client (pull-through) | Explicit import, or a scheduled sync of selected sources |
| Use case | Bandwidth / availability / air-gap | Security curation, CVE policy enforcement |
Use the upstream cache when you want apt-get install anything-from-ubuntu
to work without every machine reaching the internet. Use Import/mirror when
you want packages validated and CVE-scanned before they land in your repo.
The two are independent and can be used together.
2. Adding a cache target¶
In the UI: Cache upstream (left sidebar, under "Dépôt") → Ajouter une cible. Or via the API (admin token required):
curl -X POST https://repod.example.com/api/v1/upstream-cache/targets \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"id": "ubuntu-jammy",
"label": "Ubuntu 22.04 (jammy)",
"format": "apt",
"base_url": "http://archive.ubuntu.com/ubuntu",
"max_size_gb": 20,
"inactive_days": 30
}'
This creates a proxy at http://repod-host:8090/upstream/apt/ubuntu-jammy/
(port 8090 by default — see UPSTREAM_CACHE_PORT in .env — dedicated to
the depot-cache service, separate from the regular :80 APT repo).
Point clients at it exactly as they would the real mirror:
# /etc/apt/sources.list.d/repod-cache.list
deb http://repod-host:8090/upstream/apt/ubuntu-jammy jammy main restricted universe multiverse
deb http://repod-host:8090/upstream/apt/ubuntu-jammy jammy-updates main restricted universe multiverse
deb http://repod-host:8090/upstream/apt/ubuntu-jammy jammy-security main restricted universe multiverse
For RPM (.repo file):
[repod-almalinux9-baseos]
name=AlmaLinux 9 BaseOS (cached via repod)
baseurl=http://repod-host:8090/upstream/rpm/almalinux9-baseos
gpgcheck=1
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-9
gpgcheck/gpgkey still point at the upstream's own key — the cache
doesn't re-sign anything, so package signature verification on the client
works exactly as it would against the real mirror.
3. Bandwidth-limited sites (cache-on-demand)¶
No extra steps needed — this is the default mode. The first machine in your
fleet to apt-get install/dnf install a given package pays the cost of
fetching it from upstream; every other machine (and every repeat install,
e.g. reimaging a machine) gets it from the local cache instantly. Only
packages your fleet actually uses get cached — not the full upstream
catalog — so disk usage stays proportional to your fleet's real footprint
(see §6).
4. Fully air-gapped sites (seeding via a relay host)¶
If the Repod instance has no internet access at all, cache-on-demand
can't self-seed — there's no upstream to reach on a cache miss. The
supported workflow uses a relay host: any machine with internet access
that can run the same depot-cache container.
Step 1 — stand up the cache on the relay host¶
On the internet-connected relay (doesn't need to be a full Repod install — just the cache piece):
git clone <your-repod-repo> && cd repod
docker build -f Dockerfile.cache-nginx -t repod-cache .
mkdir -p ./repos/upstream-cache/{conf/zones,conf/locations,data}
docker run -d --name depot-cache -p 8090:80 \
-v "$(pwd)/repos/upstream-cache:/repos/upstream-cache" \
-v "$(pwd)/nginx/upstream-cache.conf:/etc/nginx/conf.d/upstream-cache.conf:ro" \
repod-cache
Then write the same target config the API would generate (or run a throwaway Repod backend against this relay's settings — simplest is to reuse the API if you have any Repod instance, even a temporary one, reachable from the relay):
curl -X POST http://localhost:8000/api/v1/upstream-cache/targets \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"id":"ubuntu-jammy","label":"Ubuntu 22.04","format":"apt",
"base_url":"http://archive.ubuntu.com/ubuntu","max_size_gb":20,"inactive_days":30}'
Step 2 — warm the cache¶
Point real client tools at the relay's cache and run your normal update/
install workflow — this is the actual, real toolchain (apt-get/dnf), no
custom package-list downloader needed:
# On a test machine pointed at the relay:
sudo sed -i 's|http://archive.ubuntu.com/ubuntu|http://relay-host:8090/upstream/apt/ubuntu-jammy|' /etc/apt/sources.list
sudo apt-get update
sudo apt-get install -y --download-only $(dpkg-query -W -f='${Package} ') # or your known package list
Repeat for every distro/release/package set your air-gapped fleet actually needs. The cache now holds every file those commands touched.
Step 3 — export and transfer¶
Move the USB drive (or any offline transfer medium your security policy allows) to the air-gapped site.
Step 4 — import on the air-gapped site¶
The air-gapped Repod instance must already have the same target
configured (same id) — create it via the API/UI exactly as in
§2 (this only writes config, no network call is
made until a client actually requests a package):
No restart is required — the imported files are picked up on the next request. Clients on the air-gapped network now get cache hits for everything you warmed on the relay, with zero internet access required.
Keeping it current¶
Repeat steps 2–4 periodically (e.g. monthly) on the relay to pick up
security updates, then re-export/re-import only the delta — tar naturally
only needs to move what changed if you keep the relay's cache directory
persistent across warm-up runs (don't re-run docker run with a fresh empty
volume each time).
5. Verifying the cache is actually working¶
Every response carries an X-Repod-Cache header:
curl -sI http://repod-host:8090/upstream/apt/ubuntu-jammy/dists/jammy/InRelease | grep X-Repod-Cache
MISS— fetched from upstream just now (first request for this path, or past its TTL).HIT— served from local cache, no upstream contact.STALE— upstream was unreachable, but a previously cached copy was still served (this is the resilience guarantee: a cached package is never blocked by an upstream outage).
To confirm resilience without waiting for a real outage, block the
container's route to the upstream host (e.g. via iptables on the Docker
host, or by temporarily editing /etc/hosts inside the container before
restart) and re-request an already-cached path — you should still get 200
and X-Repod-Cache: HIT or STALE, never a hang or a 5xx for content that
was already cached.
6. Retention, purge, and disk sizing¶
Each target has independent max_size_gb and inactive_days settings
(editable from the Cache upstream page or PATCH /upstream-cache/targets/{id}
— note: currently only enabled is patchable via the API; to change size/
retention, remove and re-add the target). Nginx evicts the least-recently-used
entries once max_size_gb is reached, and drops anything untouched for
inactive_days.
Sizing guidance:
- Cache-on-demand, one distro release, a few hundred to ~2000 similar machines: typically 5–30 GB — proportional to your fleet's actual installed footprint, not the full upstream catalog.
- A full upstream mirror (main+universe+multiverse, all pockets) would be
hundreds of GB to a few TB — the upstream cache deliberately does not
do this; use
mirror_daily(Import page → scheduled sync) if you specifically need a complete, validated local mirror instead.
Purging (Cache upstream page → Purger, or POST
/upstream-cache/targets/{id}/purge) clears the on-disk cache for one target
without touching its configuration — the next request for any previously
cached path becomes a fresh MISS. Use this after a bad upstream cache
poisoning concern, or simply to reclaim disk space.