A caching reverse proxy for package registries and artifact repositories. Single binary, single YAML config.
- 13 proxy modes in one process:
file,git,oci,npm,go,maven,cargo,pypi,flatpak,apk,deb,rpm,pacman - Path-mounted and dedicated-listener instances
- Per-resource cache policies:
bypass,immutable,revalidate - Background blob GC and expired-object cleanup
- Background metadata refresh for Flatpak/OSTree and Linux repositories (
flatpak,apk,deb,rpm,pacman) - Prometheus metrics and built-in home page
- Built-in server status modal with persisted disk history and recent scheduler/upstream events
make cache-proxy
./cache-proxy -config ./cache-proxy.yaml
./cache-proxy -config ./cache-proxy.yaml -validateMinimal config:
server:
bind: 127.0.0.1:18080
backend: /var/lib/cache-proxy
metrics:
path: /metrics
instances:
- name: npmjs
enabled: true
npm:
route: { path: /npm }
upstream: https://registry.npmjs.orgTop-level fields:
| Field | Type | Default | Description |
|---|---|---|---|
server.bind |
host:port |
127.0.0.1:18080 |
Main HTTP listener |
server.backend |
path | /tmp/cache-proxy |
Storage directory |
server.public_url |
URL | — | Public base URL shown on the home page |
server.status.disk_sample_interval |
duration | 15m |
Disk usage sampling interval for the home page status modal |
server.status.disk_history_window |
duration | 24h |
Persisted disk history retention window for the home page status modal |
server.status.event_limit |
int | 500 |
Persisted scheduler/upstream event retention limit for the home page status modal |
metrics.path |
path | /metrics |
Prometheus endpoint |
metrics.token |
string | — | Optional bearer token for /metrics |
storage.gc.blob |
duration | 24h |
Blob storage GC interval |
storage.cleanup.dry_run |
bool | false |
Run scheduled cleanup without deleting files |
storage.cleanup.batch_size |
int | 500 |
Maximum deletions per cleanup batch |
storage.orphan_policy |
string | — | Home page orphan cleanup policy (auto) |
storage.download.max_active |
int | 256 |
Global concurrent cache-fill downloads |
storage.download.max_active_per_instance |
int | 64 |
Concurrent cache-fill downloads per instance |
Value types:
| Type | Examples | Special values |
|---|---|---|
duration |
30s, 5m, 24h |
— |
expiration |
720h |
never |
freshness |
30s, 5m |
forever |
Shared instance shape:
instances:
- name: example
enabled: true
<mode>:
route: { path: /mount }
expire_after: 720h
transport:
proxy: http://127.0.0.1:7890
ua: custom-agent/1.0Notes:
- Each instance must define exactly one mode block.
- Most modes use
route.path;ociusesbind. githas its own block shape and does not useexpire_afterortransport.- The default upstream
User-Agentiscache-proxy/1; settransport.uaonly when an instance needs a custom value. transport.healthexists for upstream health tuning; active probes default toprobe_interval: 2m, reject intervals below30s, and are shared by upstream host to avoid bursty checks.- Active health probes use discovered Linux repository metadata targets; upstream roots without metadata targets are not probed actively.
- The built-in home page fetches status data from
/-/status/summary,/-/status/disk, and/-/status/events. - Linux repository modes expose discovered repository roots on the home page, including the root path, primary metadata paths, refresh state, and mode-specific attributes.
- Status history is persisted in bounded form and trimmed by
server.status.disk_history_windowandserver.status.event_limit.
| Mode | Typical use | Key fields |
|---|---|---|
file |
Generic HTTP file cache | upstreams, default_policy, rules[] |
oci |
Docker / OCI registry cache | bind, upstream, auth, rules[] |
npm |
npm registry mirror | upstream, metadata_*, tarball_policy |
go |
GOPROXY + SumDB | proxies, module_*, zip_policy, sumdb |
maven |
Maven repository cache | upstream, release_policy, snapshot_*, checksum_*, metadata_* |
cargo |
crates.io sparse index cache | upstream, crate_policy, index_* |
pypi |
PyPI simple index + files | upstream, index_*, file_policy, companion_* |
flatpak |
Flatpak / OSTree repository cache | upstreams, refresh_interval, descriptor_rewrite, verify_* |
apk |
Alpine repositories | upstreams, refresh_interval, cleanup_interval, artifact_*, auxiliary_* |
deb |
Debian / Ubuntu repositories | upstreams, refresh_interval, cleanup_interval, artifact_*, auxiliary_* |
rpm |
RPM repositories | upstreams, refresh_interval, cleanup_interval, artifact_*, auxiliary_* |
pacman |
Arch repositories | upstreams, refresh_interval, cleanup_interval, artifact_*, auxiliary_* |
git |
Read-only Git mirror | upstream, auth, proxy, sync_interval |
Client examples:
- npm:
npm config set registry http://cache.lan:8080/npm - Go:
go env -w GOPROXY=http://cache.lan:8080/go - Cargo:
registry = "sparse+http://cache.lan:8080/cargo/" - PyPI:
pip install --index-url http://cache.lan:8080/pypi/simple <pkg> - Flatpak:
flatpak remote-add --if-not-exists flathub http://cache.lan:8080/flathub/flathub.flatpakrepo - APK:
/etc/apk/repositoriesentryhttp://cache.lan:8080/apk - Debian distribution repo:
deb http://cache.lan:8080/deb bookworm main - Debian flat repo:
deb [trusted=yes] http://cache.lan:8080/deb ./ - RPM:
baseurl=http://cache.lan:8080/rpm - Pacman:
Server = http://cache.lan:8080/pacman - OCI:
docker pull cache.lan:5000/library/alpine:latest
file - generic HTTP file cache
file:
route: { path: /files }
upstreams:
- https://example.com
pass_headers: [X-Custom]
default_policy: revalidate
fresh_for: 5m
busy_policy: stale
rules:
- match: "releases/**/*.zip"
policy: immutable
expire_after: 8760hUse this mode for ordinary HTTP content where different path groups may need different cache policies.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
upstreams |
[]URL |
required | Upstream base URLs, tried in order |
pass_headers |
[]string |
— | Request headers forwarded upstream |
default_policy |
policy | bypass |
Default cache policy |
fresh_for |
freshness | — | Freshness for cached responses |
busy_policy |
busy policy | bypass |
Behavior while another request is already downloading |
rules[].match |
glob | required | Path pattern |
rules[].policy |
policy | — | Policy override |
rules[].fresh_for |
freshness | — | Freshness override |
rules[].busy_policy |
busy policy | — | Busy policy override |
rules[].expire_after |
expiration | — | Expiration override |
oci - Docker / OCI registry cache
oci:
bind: 127.0.0.1:5000
display_url: https://cache.home.lan:5000
upstream: https://registry-1.docker.io
default_policy: bypass
fresh_for: 10m
busy_policy: stale
auth:
type: basic
username: my-user
password: my-pass
rules:
- match: "library/*"
policy: immutable
expire_after: 168hUse this mode for a dedicated registry listener. Clients point Docker or other OCI tooling at the bound address.
| Field | Type | Default | Description |
|---|---|---|---|
bind |
host:port |
required | Dedicated listener |
display_url |
URL | — | Home page URL override |
upstream |
URL | required | Upstream registry |
expire_after |
expiration | 720h |
Maximum object lifetime |
default_policy |
policy | bypass |
Default cache policy |
fresh_for |
freshness | — | Freshness for cached manifests |
busy_policy |
busy policy | bypass |
Busy policy while downloading |
auth.type |
enum | — | none, basic, bearer |
auth.username |
string | — | Required for basic |
auth.password |
string | — | Required for basic |
auth.token |
string | — | Required for bearer |
rules[].match |
glob | required | Repository pattern |
rules[].policy |
policy | bypass |
Policy override |
rules[].expire_after |
expiration | — | Expiration override |
npm - npm registry mirror
npm:
route: { path: /npm }
upstream: https://registry.npmjs.org
metadata_policy: revalidate
metadata_fresh_for: 1m
metadata_busy_policy: stale
tarball_policy: immutableUse this mode for npm metadata and package tarballs behind a single prefix.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
upstream |
URL | required | Upstream registry |
metadata_policy |
policy | revalidate |
Policy for package metadata |
metadata_fresh_for |
freshness | — | Freshness for metadata |
metadata_busy_policy |
busy policy | stale |
Busy policy for metadata |
tarball_policy |
policy | immutable |
Policy for tarballs |
go - GOPROXY + SumDB
go:
route: { path: /go }
proxies:
- https://proxy.golang.org
module_policy: revalidate
module_fresh_for: 1m
module_busy_policy: stale
zip_policy: immutable
sumdb:
enabled: true
name: sum.golang.org
url: https://sum.golang.org
sumdb_fresh_for: 30s
sumdb_busy_policy: bypass
goprivate:
- "*.corp.example.com"Use this mode to proxy public module traffic while allowing selected private module prefixes to bypass the proxy.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
proxies |
[]URL |
required | GOPROXY chain |
module_policy |
policy | revalidate |
Policy for module metadata endpoints |
module_fresh_for |
freshness | 1m |
Freshness for module metadata |
module_busy_policy |
busy policy | stale |
Busy policy for module metadata |
zip_policy |
policy | immutable |
Policy for module zip files |
sumdb.enabled |
bool | true |
Enable SumDB proxying |
sumdb.name |
string | sum.golang.org |
SumDB name in request path |
sumdb.url |
URL | https://sum.golang.org |
Upstream SumDB |
sumdb_fresh_for |
freshness | 30s |
Freshness for SumDB responses |
sumdb_busy_policy |
busy policy | bypass |
Busy policy for SumDB |
goprivate |
[]glob |
— | Private module patterns that bypass proxying |
disable_module_fetch_header |
bool | false |
Honor Disable-Module-Fetch request header |
maven - Maven repository cache
maven:
route: { path: /maven }
upstream: https://repo1.maven.org/maven2
release_policy: immutable
snapshot_policy: revalidate
snapshot_fresh_for: 5m
checksum_policy: revalidate
checksum_fresh_for: 30s
checksum_busy_policy: bypass
metadata_fresh_for: 2m
metadata_busy_policy: staleUse this mode for Maven Central or an internal Maven repository with different behavior for releases, snapshots, metadata, and checksums.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
upstream |
URL | required | Upstream repository root |
release_policy |
policy | immutable |
Policy for release artifacts |
snapshot_policy |
policy | revalidate |
Policy for snapshot artifacts |
snapshot_fresh_for |
freshness | — | Freshness for snapshot artifacts |
metadata_fresh_for |
freshness | — | Freshness for maven-metadata.xml |
metadata_busy_policy |
busy policy | stale |
Busy policy for metadata |
checksum_policy |
policy | revalidate |
Policy for checksum and signature sidecars |
checksum_fresh_for |
freshness | 30s |
Freshness for checksum sidecars |
checksum_busy_policy |
busy policy | bypass |
Busy policy for checksum sidecars |
cargo - crates.io sparse index cache
cargo:
route: { path: /cargo }
upstream: https://index.crates.io
crate_policy: immutable
index_fresh_for: 5m
index_busy_policy: stale
auth_required: falseUse this mode for Cargo sparse index traffic and crate downloads.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
upstream |
URL | required | Upstream sparse index |
crate_policy |
policy | immutable |
Policy for crate files |
index_fresh_for |
freshness | — | Freshness for sparse index entries |
index_busy_policy |
busy policy | stale |
Busy policy for sparse index entries |
auth_required |
bool | false |
Return auth-required: true in generated config |
pypi - PyPI simple index and files
pypi:
route: { path: /pypi }
upstream: https://pypi.org
index_policy: revalidate
index_fresh_for: 1m
index_busy_policy: stale
file_policy: immutable
companion_policy: revalidate
companion_fresh_for: 30s
companion_busy_policy: bypass
proxy_json: true
proxy_core_metadata: false
proxy_signatures: falseUse this mode for /simple/ indexes and package file downloads, with optional sidecar proxying.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
upstream |
URL | required | Upstream PyPI base URL |
index_policy |
policy | revalidate |
Policy for simple index pages |
index_fresh_for |
freshness | 1m |
Freshness for simple index pages |
index_busy_policy |
busy policy | stale |
Busy policy for index pages |
file_policy |
policy | immutable |
Policy for package files |
companion_policy |
policy | revalidate |
Policy for sidecar files |
companion_fresh_for |
freshness | 30s |
Freshness for sidecars |
companion_busy_policy |
busy policy | bypass |
Busy policy for sidecars |
proxy_json |
bool | true |
Enable /simple/<pkg>/json |
proxy_core_metadata |
bool | false |
Proxy metadata sidecars |
proxy_signatures |
bool | false |
Proxy signature sidecars |
flatpak - Flatpak / OSTree repository cache
flatpak:
route: { path: /flathub }
upstreams:
- https://dl.flathub.org/repo
refresh_interval: 5m
cleanup_interval: 6h
descriptor_rewrite: true
verify_objects: true
cache_deltas: true
delta_expire_after: 720hUse this mode for Flatpak repositories backed by OSTree. summary, summary.sig, and config are refreshed as metadata generations from one upstream. OSTree objects are cached outside generations and verified before immutable cache writes. Static deltas are cached as opaque immutable files by path and are validated by Flatpak/OSTree clients when applied.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
expire_after |
expiration | 720h |
Maximum object lifetime |
upstreams |
[]URL |
required | Upstream OSTree repository URLs |
refresh_interval |
duration | 5m |
Background summary refresh interval |
cleanup_interval |
duration | 6h |
Expired-object cleanup interval |
metadata_fresh_for |
freshness | 1m |
Freshness for metadata fallback cache |
metadata_busy_policy |
busy policy | stale |
Busy policy for metadata fallback cache |
descriptor_rewrite |
bool | true |
Rewrite .flatpakrepo / .flatpakref URLs to the proxy |
verify_objects |
bool | true |
Verify OSTree objects before immutable cache writes |
cache_deltas |
bool | true |
Cache deltas/** as opaque immutable files |
delta_expire_after |
expiration | inherits expire_after |
Maximum delta object lifetime; never is rejected when delta caching is enabled |
apk - Alpine repository cache
apk:
route: { path: /apk }
upstreams:
- https://dl-cdn.alpinelinux.org/alpine
refresh_interval: 1h
cleanup_interval: 6h
artifact_policy: immutable
auxiliary_policy: revalidateUse this mode for Alpine repositories discovered from APKINDEX.tar.gz requests. The repository root is the directory that contains APKINDEX.tar.gz.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
upstreams |
[]URL |
required | Upstream mirrors |
refresh_interval |
duration | 1h |
Background metadata refresh interval |
cleanup_interval |
duration | 6h |
Indexed cleanup interval |
artifact_policy |
policy | immutable |
Policy for package files inside the current published generation |
artifact_fresh_for |
freshness | — | Freshness for package files |
artifact_busy_policy |
busy policy | bypass |
Busy policy for package files |
artifact_expire_after |
expiration | — | Expiration override for package files |
auxiliary_policy |
policy | revalidate |
Policy for auxiliary files inside the current published generation |
auxiliary_fresh_for |
freshness | 30s |
Freshness for auxiliary files |
auxiliary_busy_policy |
busy policy | bypass |
Busy policy for auxiliary files |
auxiliary_expire_after |
expiration | — | Expiration override for auxiliary files |
deb - Debian / Ubuntu repository cache
deb:
route: { path: /deb }
upstreams:
- https://deb.debian.org/debian
refresh_interval: 1h
cleanup_interval: 6h
artifact_policy: immutable
auxiliary_policy: revalidateUse this mode for Debian-style repositories discovered from Release, InRelease, Packages*, and Sources*
metadata requests. Both standard dists/<suite>/... layouts and flat repositories are supported.
Standard apt sources use deb <proxy-url> <suite> <component>. Flat repositories use
deb [trusted=yes] <proxy-url> ./. In both cases, <proxy-url> is the cache-proxy HTTP(S) URL, not a
local filesystem path.
Same field set as apk.
rpm - RPM repository cache
rpm:
route: { path: /rpm }
upstreams:
- https://download.rockylinux.org/pub/rocky
refresh_interval: 1h
cleanup_interval: 6h
artifact_policy: immutable
auxiliary_policy: revalidateUse this mode for RPM repositories discovered from repodata/repomd.xml.
Same field set as apk.
pacman - Arch repository cache
pacman:
route: { path: /pacman }
upstreams:
- https://geo.mirror.pkgbuild.com
refresh_interval: 2m
cleanup_interval: 6h
artifact_policy: immutable
auxiliary_policy: revalidateUse this mode for Arch repositories discovered from repository database requests such as .db and .db.tar.*.
Same field set as apk, except refresh_interval defaults to 2m.
git - read-only Git mirror
git:
route: { path: /git }
upstream: https://github.com/user/repo.git
auth:
type: token
password: $GITHUB_TOKEN
proxy: socks5://proxy:1080
sync_interval: 5m
force_overwrite: trueUse this mode for a single upstream Git repository mirrored behind an HTTP path.
| Field | Type | Default | Description |
|---|---|---|---|
route.path |
path | required | URL mount path |
upstream |
URL | required | Remote Git repository |
auth.type |
enum | — | basic or token |
auth.username |
string | — | Username for basic auth |
auth.password |
string | — | Password or token, supports $ENV expansion |
proxy |
URL | — | HTTP or SOCKS5 proxy for upstream access |
sync_interval |
duration | 0 |
Periodic sync interval; 0 means no background sync |
operation_timeout |
duration | 0 |
Per clone/fetch timeout |
force_overwrite |
bool | true |
Overwrite local refs after upstream force-pushes |
flatpak uses background summary refresh and a dedicated cache layout:
summary,summary.sig, andconfigare published as a metadata generation from one upstream.- OSTree objects under
objects/are cached outside metadata generations for reuse across updates. - Verifiable OSTree objects must pass checksum validation before immutable cache writes.
- Static deltas under
deltas/are cached as opaque immutable files by path; cache-proxy does not parse or verify delta contents, and Flatpak/OSTree clients validate them while applying updates. .flatpakrepoand.flatpakrefdescriptors are revalidated and can be rewritten to keep clients on the proxy.
apk, deb, rpm, and pacman use background metadata refresh:
- Repositories are discovered from client metadata requests.
- Discovered repositories are persisted, and startup reconciles refresh tasks from the persisted repository set.
- Metadata is published only after a full generation is fetched and validated.
- The current generation is the authoritative serving view for repository metadata, including companion files such as signatures and checksums.
- If no local generation exists yet, metadata requests bypass to upstream and trigger background refresh.
- Artifact and auxiliary cache objects are generation-scoped. Switching current generation changes visible repository content immediately without in-place overwrites.
- Artifact and auxiliary downloads are still independent requests; they are not blocked by index misses or refresh failure.
- Cleanup removes obsolete non-generation repository objects, and metadata GC removes superseded generations. No package index is persisted for runtime validation.
- Run behind a TLS-terminating reverse proxy if exposed outside localhost.
- Keep config files private; they may contain upstream credentials.
- Set
metrics.tokenif/metricsis reachable by other hosts. - Restart the process to apply configuration changes.
make fmt
make test