git-pkgs proxy
A caching proxy for package registries. Speeds up package downloads by caching artifacts locally, reducing bandwidth usage and improving reliability.
Version Cooldown
Most supply chain attacks rely on speed: a malicious version gets published and consumed by automated pipelines within minutes, before anyone notices. The cooldown feature adds a quarantine period to newly published versions. When enabled, the proxy strips versions from metadata responses until they've aged past a configurable threshold.
cooldown:
default: "3d" # hide versions published less than 3 days ago
ecosystems:
npm: "7d" # npm gets a longer window
cargo: "0" # disable for cargo
packages:
"pkg:npm/lodash": "0" # exempt trusted packages
A 3-day cooldown means that when lodash publishes version 4.18.0, your builds keep using 4.17.21 until 3 days have passed. If the new release turns out to be compromised, you were never exposed.
Resolution order: package override, then ecosystem override, then global default. This lets you set a conservative default and carve out exceptions for packages where you need faster updates. See docs/configuration.md for the full config reference.
One instance can offer several cooldown policies through named profiles. Configure cooldown.profiles and point clients at paths such as /cooldown/3d/npm/ or /cooldown/7d/cargo/. Profiles share the package cache; existing URLs keep using the global policy.
Artifact Scanning
Cooldown only looks at a version's publish timestamp — it never inspects the actual bytes. Artifact scanning closes that gap: when enabled, every artifact is staged into storage and scanned by one or more external services (trivy, ClamAV, Wiz, or anything else that speaks a small HTTP/JSON contract) before it's committed to the cache and served to clients.
scanning:
enabled: true
signing_key: ${PROXY_SCANNING_SIGNING_KEY}
scanners:
- name: clamav
url: http://clamav-adapter:8080/scan
mode: block # a block verdict deletes the artifact and returns 403
- name: trivy
url: http://trivy-adapter:8081/scan
mode: monitor # findings are logged, never gate caching
ecosystems: [npm, pypi]
The proxy never uploads artifact bytes to a scanner. Each scanner is notified with package metadata plus a short-lived signed URL; the scanner pulls the bytes itself from the proxy's own storage. Scanners run concurrently, and the first block-mode scanner to report a verdict of not-allowed wins immediately, canceling the rest. See docs/configuration.md for the full config reference and the scanner HTTP contract.
Supported Registries
| Registry | Language/Platform | Cooldown | Completed | |----------|-------------------|:--------:|:---------:| | npm | JavaScript | Yes | ✓ | | Cargo | Rust | Yes | ✓ | | RubyGems | Ruby | Yes | ✓ | | Go proxy | Go | Yes† | ✓ | | Hex | Elixir | Yes* | ✓ | | pub.dev | Dart | Yes | ✓ | | PyPI | Python | Yes | ✓ | | Maven | Java | Yes‡ | ✓ | | Gradle Build Cache | Java/Kotlin | | ✓ | | NuGet | .NET | Yes | ✓ | | Composer | PHP | Yes | ✓ | | Conan | C/C++ | | ✓ | | Conda | Python/R | Yes | ✓ | | CRAN | R | | ✓ | | Julia | Julia | | ✓ | | Swift | Swift | | ✓ | | Container | Docker/OCI | | ✓ | | Homebrew | macOS/Linux | | ✓ | | Debian | Debian/Ubuntu | | ✓ | | RPM | RHEL/Fedora | | ✓ | | Alpine | Alpine Linux | | ✓ | | Arch | Arch Linux | | ✓ | | FreeBSD | FreeBSD (ports and pkgbase) | | ✓ | | Chef Supermarket | Chef | | ✓ | | Generic | Any | | ✓ | | Git | Any | | ✓ | | Helm | Kubernetes | Yes | ✓ | | Vagrant | Vagrant | | ✗ |
Cooldown requires publish timestamps in metadata. Registries without a "Yes" in the cooldown column either don't expose timestamps or haven't been wired up yet.
\* Hex cooldown requires disabling registry signature verification (HEX_UNSAFE_REGISTRY=1) since the proxy re-encodes the protobuf payload. Origin verification can stay enabled.
† Go cooldown uses the time in each version's .info, which is the commit time of the tagged revision, not when the tag was pushed.
‡ Maven cooldown uses the Last-Modified header of each version's .pom. On Maven Central that is the deploy time; a Nexus or Artifactory upstream may report when it cached the file instead. SNAPSHOT versions are not held back.
Install
brew install git-pkgs/git-pkgs/proxy
Or download a binary from the releases page, or run the container image:
docker run -p 8080:8080 -v proxy-data:/data ghcr.io/git-pkgs/proxy:latest
Helm
Install the chart from GHCR, setting the public URL that package-manager clients will use to reach the proxy:
helm install proxy oci://ghcr.io/git-pkgs/charts/proxy \
--set config.data.base_url=https://proxy.example.com
The default chart deploys one replica backed by a 10 GiB persistent volume,
using SQLite and filesystem artifact storage under /data. See
deploy/charts/proxy/values.yaml for ingress,
external database and object-storage configuration options.
The SQLite database needs a local disk or a block-backed volume, even with one
replica; ReadWriteOnce alone doesn't guarantee that, since the storage class
can still be NFS. If the cluster only offers network filesystem storage, use
Postgres. Artifact storage can stay on NFS, separately from the database. See
SQLite.
To trust a private CA, for example when upstreams sit behind a TLS-intercepting
corporate proxy or are internal registries signed by an in-house CA, mount the
certificate with extraVolumes and extraVolumeMounts and add its directory
to SSL_CERT_DIR. The proxy process makes all upstream TLS connections itself,
Git fetches included, so no other configuration is needed. Public CAs stay
trusted alongside the private one:
extraVolumes:
- name: corp-ca
configMap:
name: corp-ca # e.g. key corp-ca.crt, PEM-encoded
extraVolumeMounts:
- name: corp-ca
mountPath: /etc/proxy/ca
readOnly: true
extraEnv:
- name: SSL_CERT_DIR
value: /etc/ssl/certs:/etc/proxy/ca
Quick Start
# Build from source
go build -o proxy ./cmd/proxy
Run with defaults (listens on :8080)
./proxy
Run with custom settings
./proxy -listen :3000 -base-url https://proxy.example.com
The proxy is now running. Configure your package managers to use it. For image builds, see Using proxy in Docker builds.
OpenAPI (Swagger)
This repo uses swaggo to generate an OpenAPI spec from annotated handlers.
Generate the spec:
go install github.com/swaggo/swag/cmd/swag@latest
go generate ./internal/server
Generated files are written to docs/swagger/.
When the proxy is running, fetch the live spec from:
http://localhost:8080/openapi.json
http://localhost:8080 with your configured base URL. This link is also shown on the dashboard.
Configuring Package Managers
npm
Create or edit ~/.npmrc:
registry=http://localhost:8080/npm/
Or set per-project in .npmrc:
registry=http://localhost:8080/npm/
Or use environment variable:
npm_config_registry=http://localhost:8080/npm/ npm install
npm audit, pnpm audit, yarn npm audit and npm audit signatures work
through the proxy: the audit and signing-key endpoints are passed through to the
configured upstream registry, with upstream authentication applied. Advisories
therefore come from upstream's database, not from the proxy's own vulnerability
data, and versions withheld by cooldown are not excluded
from the report. Packuments point each version's attestations (provenance) at
the proxy, which fetches them from the package's registry and caches them;
attestations of versions held back by cooldown or blocked by the denylist are
not served.
To serve private packages through the same registry URL, see Private package routes. To see new releases before metadata_ttl runs out, see Change feeds.
Cargo
Create or edit ~/.cargo/config.toml:
[source.crates-io]
replace-with = "proxy"
[source.proxy]
registry = "sparse+http://localhost:8080/cargo/"
Or set per-project in .cargo/config.toml in your project root.
RubyGems / Bundler
Set the gem source in your Gemfile:
source "http://localhost:8080/gem"
Or configure globally:
gem sources --add http://localhost:8080/gem/
bundle config mirror.https://rubygems.org http://localhost:8080/gem
Go modules
Set the GOPROXY environment variable:
export GOPROXY=http://localhost:8080/go,direct
Or in your shell profile for persistence.
Checksum lookups go through the same URL: the proxy serves sum.golang.org at
/go/sumdb/sum.golang.org/, and the go command uses it without further
setup, so clients need no route of their own to sum.golang.org. If the
proxy loses https://sum.golang.org, it keeps answering until its cached
copy of the database's signed tree is older than metadata_ttl; after that
the go command contacts the database directly as before. Set
upstream.go_sumdbs_offline on networks where clients cannot do that. Other
checksum databases are listed under upstream.go_sumdbs, see
Go checksum database.
govulncheck can use the proxy for the Go vulnerability database:
govulncheck -db http://localhost:8080/go/vulndb ./...
See Go vulnerability database.
Homebrew
Point Homebrew's JSON API and artifact domain at the proxy:
export HOMEBREW_API_DOMAIN=http://localhost:8080/homebrew
export HOMEBREW_ARTIFACT_DOMAIN=http://localhost:8080
The artifact domain proxies manifests and bottle blobs under /v2/homebrew/core/. GHCR routing is limited to that repository. Source archives, cask application downloads, custom tap artifacts, and legacy flat-file bottle mirrors use Homebrew's normal fallback URLs. Keep fallback enabled by leaving HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK unset.
Enable cache_metadata or set PROXY_CACHE_METADATA=true to retain Homebrew JSON API responses for offline fallback. Bottle blobs and their OCI manifests are cached without this setting. proxy mirror pkg:brew/ pre-fetches a formula's bottles (see Homebrew bottles).
The upstreams default to https://formulae.brew.sh/api for the JSON API and https://ghcr.io for artifacts. To chain this proxy to another proxy, configure its Homebrew endpoints as the upstreams:
upstream:
homebrew_api: "https://upstream-proxy.example.com/homebrew"
homebrew_artifact: "https://upstream-proxy.example.com"
The equivalent environment variables are PROXY_UPSTREAM_HOMEBREW_API and PROXY_UPSTREAM_HOMEBREW_ARTIFACT.
Hex (Elixir)
Configure the mirror:
mix hex.config mirror_url http://localhost:8080/hex
Or set the environment variable:
export HEX_MIRROR=http://localhost:8080/hex
When the proxy applies a cooldown, also set:
export HEX_UNSAFE_REGISTRY=1
Filtering releases removes the registry signature. The proxy preserves the package name and repository fields, so origin verification can stay enabled. Without a cooldown, leave signature verification enabled too.
pub.dev (Dart/Flutter)
Set the PUB_HOSTED_URL environment variable:
export PUB_HOSTED_URL=http://localhost:8080/pub
PyPI (pip)
Configure pip to use the proxy:
pip install --index-url http://localhost:8080/pypi/simple/ package_name
Or set in ~/.pip/pip.conf:
[global]
index-url = http://localhost:8080/pypi/simple/
Maven
Add to your ~/.m2/settings.xml:
<settings>
<mirrors>
<mirror>
<id>proxy</id>
<mirrorOf>central</mirrorOf>
<url>http://localhost:8080/maven/</url>
</mirror>
</mirrors>
</settings>
The /maven/ endpoint uses Maven Central as primary upstream and falls back to the Gradle Plugin Portal for Gradle plugin marker metadata and related artifacts when the primary upstream returns not found.
Further Maven repositories can be configured under upstream.maven_repositories and are served at /maven/{name}/, each from its own upstream (see Maven repositories). Point a second or Gradle maven(url = ...) entry at http://localhost:8080/maven/{name}/.
For Gradle plugin resolution via the same proxy endpoint:
pluginManagement {
repositories {
maven(url = "http://localhost:8080/maven/")
}
}
Gradle HTTP Build Cache
Configure in settings.gradle.kts:
buildCache {
remote<HttpBuildCache> {
url = uri("http://localhost:8080/gradle/")
}
}
Enable build caching with org.gradle.caching=true in gradle.properties, or pass --build-cache when running Gradle. The local cache stays enabled and remote uploads are disabled by default.
To upload from CI, set gradle.build_cache.read_only to false on the proxy and add isPush = System.getenv("CI") != null inside the client's remote block. See docs/configuration.md.
NuGet
Configure in nuget.config:
<configuration>
<packageSources>
<clear />
<add key="proxy" value="http://localhost:8080/nuget/v3/index.json" />
</packageSources>
</configuration>
Or use the CLI:
dotnet nuget add source http://localhost:8080/nuget/v3/index.json -n proxy
Composer (PHP)
Configure in composer.json:
{
"repositories": [
{
"type": "composer",
"url": "http://localhost:8080/composer"
}
]
}
Or set globally:
composer config -g repositories.proxy composer http://localhost:8080/composer
composer audit, audit.block-insecure and Packagist's malware filter list
work through the proxy when upstream.composer_repository offers them in its
packages.json, as repo.packagist.org does. The proxy advertises them in its
own packages.json and passes the advisories and filter list requests through
to the upstream uncached. Names of packages in upstream.composer_routes or
hosted.composer_vendors are removed from advisories requests before they are
sent. Versions withheld by cooldown are not excluded from
the report.
To serve private packages through the same URL, see Private package routes. To see new releases before metadata_ttl runs out, see Change feeds.
Conan (C/C++)
Add the proxy as a remote:
conan remote add proxy http://localhost:8080/conan
conan remote disable conancenter
Or configure in ~/.conan2/remotes.json.
Chef Supermarket (Berkshelf, Policyfiles)
Point Berkshelf at the proxy in your Berksfile:
source "http://localhost:8080/chef"
Or set the default source of a Policyfile:
default_source :supermarket, "http://localhost:8080/chef"
The proxy serves the /universe dependency index and the cookbook API, rewriting every download location to itself while leaving dependency constraints unchanged. Cookbook archives are fetched through the Supermarket download redirect and cached, so they stay available when the Supermarket is down. With cache_metadata on, the universe and cookbook metadata follow the usual TTL and stale fallback.
To proxy a private Supermarket instead of supermarket.chef.io, set upstream.chef (or PROXY_UPSTREAM_CHEF) and add credentials under upstream.auth if it needs them. See docs/configuration.md.
Conda
Configure in ~/.condarc:
channels:
- http://localhost:8080/conda/main
- http://localhost:8080/conda/conda-forge
default_channels:
- http://localhost:8080/conda/main
default_channels also routes requests for defaults through the proxy, including -c defaults and environment.yml files that name it.
Or set via command:
conda config --add channels http://localhost:8080/conda/main
conda config --add default_channels http://localhost:8080/conda/main
pixi and other rattler-based clients use sharded repodata (CEP 16) where a channel offers it, as conda-forge does. Point them at the same channel URLs, for example pixi init -c http://localhost:8080/conda/conda-forge. The shard index and shards are cached as metadata when cache_metadata is on.
CRAN (R)
Set the repository in R:
options(repos = c(CRAN = "http://localhost:8080/cran"))
Or in ~/.Rprofile for persistence:
local({
r <- getOption("repos")
r["CRAN"] <- "http://localhost:8080/cran"
options(repos = r)
})
Source packages, Windows and macOS binaries (including the bin/macosx/big-sur-arm64/ layout used by R 4.3 and later) are cached as artifacts. PACKAGES indexes and the src/contrib/Meta/*.rds files that remotes::install_version and pak read are cached as metadata.
Julia
Set the Pkg server before starting Julia:
export JULIA_PKG_SERVER=http://localhost:8080/julia
Or inside a running session:
ENV["JULIA_PKG_SERVER"] = "http://localhost:8080/julia"
using Pkg; Pkg.update()
Swift
Configure the proxy as the default registry for the current Swift package:
swift package-registry set --allow-insecure-http http://localhost:8080/swift
Registry dependencies use their scoped package identifier in Package.swift:
dependencies: [
.package(id: "apple.swift-argument-parser", from: "1.2.0")
]
The proxy supports dependency resolution and source downloads. Publishing with
swift package-registry publish is not supported.
Docker / Container Registry
Configure Docker to use the proxy as a registry mirror in /etc/docker/daemon.json:
{
"registry-mirrors": ["http://localhost:8080"]
}
Then restart Docker:
sudo systemctl restart docker
Or pull images directly:
docker pull localhost:8080/library/nginx:latest
The registry is pull-only. BuildKit cache exports (--cache-to type=registry)
and other pushes need a separate registry.
SBOMs, signatures and attestations attached to an image are found through the
OCI 1.1 referrers API (GET /v2/{name}/referrers/{digest}). oras discover
and notation verify use it, and so does cosign for its newer signature
bundles. The proxy forwards these requests and caches the returned index with
the same TTL as other metadata, so they keep working offline. It does not
filter by artifactType; it returns the full list and the client filters it,
as the spec allows. Registries without the referrers API (GHCR, for example)
answer 404, and clients then look up the sha256- tag instead, which
the proxy serves like any other manifest. The proxy is pull-only, so attaching
new artifacts through it is not supported.
containerd (Kubernetes, k3s, nerdctl)
containerd mirrors send the original registry host in an ns query
parameter, so one mirror entry can serve Docker Hub and the registries
configured in upstream.oci whose URL has no path. Point containerd's CRI
plugin at a hosts directory in /etc/containerd/config.toml and restart
containerd. For containerd 2.x:
version = 3
[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/containerd/certs.d"
For containerd 1.x:
version = 2
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
Then create /etc/containerd/certs.d/_default/hosts.toml:
[host."http://proxy.example.com:8080"]
capabilities = ["pull", "resolve"]
To check that CRI picked up the directory, confirm the setting containerd
runs with and pull through CRI (on k3s use k3s crictl):
containerd config dump | grep config_path
crictl pull docker.io/library/nginx:latest
The proxy logs a container manifest request line for the pull and, when
access_log.path is set, an access-log entry. ctr images pull --hosts-dir
is no substitute for this check: it reads the directory itself and succeeds
even while CRI still has no config_path.
docker.io and the host of upstream.oci_default use the default registry;
the host of each upstream.oci URL uses that named registry, and so do the
hosts listed for it in upstream.oci_mirrors. Pulls for any other registry get
404 NAME_UNKNOWN, and containerd falls back to the registry itself. Existing
per-registry hosts.toml files that point at /v2/upstream/{name} with
override_path = true keep working for that upstream's own host and for
Docker Hub. If the upstream is a mirror of the registry the nodes pull from,
say an Artifactory remote of ghcr.io, list that registry in
upstream.oci_mirrors (ghcr: ["ghcr.io"]); otherwise those pulls get the
404 as well. See docs/configuration.md for how
hosts are matched and which entry wins when two share a host. k3s generates
the hosts directory itself from /etc/rancher/k3s/registries.yaml; mirrors:
{"*": {endpoint: ["http://proxy.example.com:8080"]}} produces the same
_default entry.
Helm
Configure each HTTP chart repository with a name, then add the matching proxy URL to Helm:
upstream:
helm:
bitnami: "https://charts.bitnami.com/bitnami"
helm repo add bitnami http://localhost:8080/helm/bitnami
helm repo update
helm pull bitnami/nginx
The proxy caches index.yaml using the normal metadata-cache settings and
caches chart archives after verifying their SHA-256 digest from the index.
For charts stored in an OCI registry, configure a named OCI upstream and add
the reserved upstream/{name} prefix to the chart reference:
upstream:
oci:
ghcr: "https://ghcr.io"
helm pull oci://localhost:8080/upstream/ghcr/owner/charts/mychart --version 1.0.0 --plain-http
Debian / APT
Configure APT to use the proxy in /etc/apt/sources.list.d/proxy.list:
deb http://localhost:8080/debian stable main contrib
Or in deb822 format, in /etc/apt/sources.list.d/proxy.sources:
Types: deb
URIs: http://localhost:8080/debian
Suites: stable
Components: main contrib
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Replace your existing sources.list entries (on newer releases also
debian.sources or ubuntu.sources in /etc/apt/sources.list.d/), then:
sudo apt update
There is no proxy key to install. The proxy serves the upstream's signed
InRelease and Release files unchanged, so APT verifies them with the
upstream archive's own key from the distribution's keyring package:
debian-archive-keyring on Debian, ubuntu-keyring on Ubuntu
(/usr/share/keyrings/ubuntu-archive-keyring.gpg). Signed-By: in a
.sources file, or [signed-by=...] in a .list line, restricts the source
to that key. For a third-party upstream, use the key its vendor publishes.
The upstream defaults to http://deb.debian.org/debian. To proxy a different APT repository (e.g. Ubuntu), set upstream.debian in the config file or PROXY_UPSTREAM_DEBIAN in the environment:
upstream:
debian: "http://archive.ubuntu.com/ubuntu"
A release's security updates are served by a separate archive, so additional
archives are configured under upstream.debian_repositories and served at
/debian/{name}/:
upstream:
debian: "http://deb.debian.org/debian"
debian_repositories:
security: "https://security.debian.org/debian-security"
deb http://localhost:8080/debian trixie main
deb http://localhost:8080/debian/security trixie-security main
upstream.debian keeps serving /debian/pool/… and /debian/dists/…
unchanged. See Debian archives.
The version denylist blocks recognized .deb downloads but leaves signed APT
indexes unchanged. To keep APT from selecting a denied version, configure
client-side version pinning.
RPM / Yum / DNF
In the [fedora] section of your existing repository file under /etc/yum.repos.d/, replace the metalink or mirrorlist entry with:
baseurl=http://localhost:8080/rpm/releases/$releasever/Everything/$basearch/os/
In the [updates] section of fedora-updates.repo, replace its metalink or mirrorlist entry with:
baseurl=http://localhost:8080/rpm/updates/$releasever/Everything/$basearch/
Keep the existing gpgkey and signature settings, including gpgcheck=1. The proxy serves RPM packages unchanged, so their signatures remain valid.
Then:
sudo dnf clean all
sudo dnf update
Alpine / apk
Point /etc/apk/repositories at the proxy. The default repository name
alpine proxies the official mirror (https://dl-cdn.alpinelinux.org/alpine):
http://localhost:8080/apk/alpine/v3.22/main
http://localhost:8080/apk/alpine/v3.22/community
Then:
apk update
Repository indexes (v2 APKINDEX.tar.gz and v3 Packages.adb), detached
signatures, and packages are served byte-for-byte unchanged, so apk's normal
signature verification keeps working. Indexes use the metadata cache
(metadata_ttl, stale fallback); .apk packages are stored in the shared
artifact cache and remain available when the upstream is unreachable.
Set upstream.apk_snapshots (e.g. "90d") to keep replaced indexes and serve
them by date at /apk/alpine@2026-10-01/v3.22/main, so pinned package versions
stay installable after the mirror moves on. See
Alpine APK snapshots.
To proxy other mirrors or private repositories, configure named upstreams
under upstream.apk (this replaces the built-in default; re-add alpine if
you still want it):
upstream:
apk:
alpine: "https://dl-cdn.alpinelinux.org/alpine"
private: "https://apk.example.com"
http://localhost:8080/apk/private
apk appends the architecture and index filename to each repository line itself.
Arch Linux / pacman
Put the proxy at the top of /etc/pacman.d/mirrorlist. The default repository
name archlinux proxies the official GeoIP mirror
(https://geo.mirror.pkgbuild.com):
Server = http://localhost:8080/arch/archlinux/$repo/os/$arch
Then:
pacman -Syu
Repository databases (core.db, core.files and their .tar.* forms),
database signatures, packages and package signatures are served byte-for-byte
unchanged, so pacman's signature checks keep working. Databases use the
metadata cache (metadata_ttl, stale fallback). Each database refresh also
fetches its signature and stores it for exactly that database, so a signature
is only ever served with the database it signs. Enable cache_metadata (or set PROXY_CACHE_METADATA=true) so
pacman -Sy keeps working while the upstream is down; without it databases are
fetched fresh on every request. Packages and their .sig files are stored in the shared artifact
cache and remain available when the upstream is unreachable.
The proxy only starts sending a package once it has the whole file, and pacman
gives up on a download that sends nothing for 10 seconds. The fetch keeps
running and the next try is served from the cache. On a slow upstream, set
DisableDownloadTimeout in /etc/pacman.conf to avoid the failed first try.
extra.files is about 50 MB and is read into memory on each pacman -F
request. Raise metadata_max_size (100 MB by default) for larger custom
repositories.
A custom repository that rebuilds a package under the same version leaves the
old package and signature in the cache. Bump pkgrel instead, or remove the
cached version.
Arch Linux ARM
Arch Linux ARM has no built-in repository. Add one under upstream.arch, using a
regional mirror from
upstream:
arch:
archlinux: "https://geo.mirror.pkgbuild.com"
archlinuxarm: "https://<region>.mirror.archlinuxarm.org"
Setting upstream.arch replaces the built-in default, so keep archlinux in the map
if you also proxy x86_64 Arch.
Arch Linux ARM mirrors use $arch/$repo instead of $repo/os/$arch. In
/etc/pacman.d/mirrorlist:
Server = http://localhost:8080/arch/archlinuxarm/$arch/$repo
Avoid mirror.archlinuxarm.org as the upstream. It redirects each request to a
changing regional mirror over plain HTTP, so the database and the packages can come
from mirrors with a different sync state.
FreeBSD / pkg
Override the repositories in /usr/local/etc/pkg/repos/FreeBSD.conf. Settings
there are merged with /etc/pkg/FreeBSD.conf, so only the URL and mirror type
change and the signature settings stay. The default repository name freebsd
proxies the official mirror (https://pkg.freebsd.org). On FreeBSD 15:
FreeBSD-ports: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/quarterly",
mirror_type: "none"
}
FreeBSD-ports-kmods: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/kmods_quarterly_${VERSION_MINOR}",
mirror_type: "none"
}
FreeBSD-base: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/base_release_${VERSION_MINOR}",
mirror_type: "none"
}
FreeBSD 14 has a single repository called FreeBSD:
FreeBSD: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/quarterly",
mirror_type: "none"
}
Then:
pkg update
Keep the path after ${ABI} that pkg -vv shows for each repository
(quarterly, latest, base_latest, ...) and replace only the host part.
The repository catalogues (meta.conf, packagesite.pkg, data.pkg) carry
their signature inside the archive and are served byte-for-byte unchanged
through the metadata cache (metadata_ttl, stale fallback). Enable
cache_metadata so pkg update keeps working while the upstream is down.
Packages under Hashed/ have their content hash in the file name, so they are
stored in the shared artifact cache as pkg:bsd/freebsd/{name}@{version} and
remain available when the upstream is unreachable.
Other files are passed through uncached. That includes the bootstrap package
under Latest/, whose signature file is rewritten with every repository build,
and package files without a hash in their name, which can be rebuilt under the
same name. The denylist still applies to unhashed package files. Since only
cached artifacts can be scanned, both kinds of package file are refused while
a scanner covers freebsd; use repositories built with pkg repo --hash and
bootstrap pkg without the proxy.
GitHub Releases / mise (aqua backend)
Configure named generic upstreams:
upstream:
generic:
github: "https://github.com"
github-api: "https://api.github.com"
Then rewrite GitHub URLs in mise's settings (~/.config/mise/config.toml, mise ≥ 2025.9.3):
[settings.url_replacements]
"regex:^https://github\\.com/([^/]+)/([^/]+)/releases/download/(.+)" = "http://localhost:8080/generic/github/$1/$2/releases/download/$3"
"regex:^https://api\\.github\\.com/(.*)" = "http://localhost:8080/generic/github-api/$1"
Release assets are cached permanently after the first download and keep
installing while GitHub is down. Tag lookups through api.github.com are
cached for metadata_ttl and served stale during an outage or rate limit.
Commit a mise.lock and install with mise install --locked so pinned
installs need no API call at all.
If the fleet exceeds GitHub's anonymous rate limit, add a bearer token for
https://api.github.com under upstream.auth. Everyone who can reach the
proxy can then make requests with that token, so the proxy requires
generic_paths listing the API paths it may serve, and the token should be a
fine-grained one with read-only access to public repositories:
upstream:
generic_paths:
github-api:
- "repos///releases"
- "repos///releases/**"
- "repos///tags"
auth:
"https://api.github.com":
type: bearer
token: "${GITHUB_TOKEN}"
Maven, mvnd, Node and Go downloads are cached the same way once their hosts are configured as generic upstreams; their version-pinned paths are built in (see configuration):
upstream:
generic:
apache: "https://archive.apache.org"
nodejs: "https://nodejs.org"
golang: "https://dl.google.com/go"
[settings]
node.mirror_url = "http://localhost:8080/generic/nodejs/dist/"
go.download_mirror = "http://localhost:8080/generic/golang"
[settings.url_replacements]
"https://archive.apache.org/" = "http://localhost:8080/generic/apache/"
Other download hosts need their version-pinned paths declared in
upstream.generic_artifacts.
Git repositories
Configure named Git servers:
upstream:
git:
github: "https://github.com"
Then point clones at the proxy:
git config --global url."http://localhost:8080/git/github/".insteadOf https://github.com/
Each repository is cached as a bare clone and refreshed whenever its branches or tags change upstream. Git LFS objects are cached in the artifact cache; git-lfs uses the proxy for them with no further setup. See configuration for setting the rewrite through environment variables, size limits, private repositories and outage behavior.
Configuration
The proxy can be configured via:
- Command line flags (highest priority)
- Environment variables
- Configuration file (YAML or JSON)
Command Line Flags
-config string Path to configuration file
-listen string Address to listen on (default ":8080")
-base-url string Public URL of this proxy (default "http://localhost:8080")
-storage-url string Storage URL (file://, s3://, gs://, azblob://)
-storage-path string Path to artifact storage directory (deprecated, use -storage-url)
-database-driver string Database driver: sqlite or postgres (default "sqlite")
-database-path string Path to SQLite database file (default "./cache/proxy.db")
-database-url string PostgreSQL connection URL
-log-level string Log level: debug, info, warn, error (default "info")
-log-format string Log format: text, json (default "text")
-access-log string Path to the JSONL access log
-version Print version and exit
Environment Variables
PROXY_LISTEN=:8080
PROXY_BASE_URL=http://localhost:8080
PROXY_UI_URL=http://localhost:8080 # Optional; defaults to PROXY_BASE_URL
PROXY_STORAGE_URL=file:///var/cache/proxy/artifacts
PROXY_DATABASE_DRIVER=sqlite
PROXY_DATABASE_PATH=./cache/proxy.db
PROXY_DATABASE_URL=postgres://user:pass@localhost/proxy?sslmode=disable
PROXY_LOG_LEVEL=info
PROXY_LOG_FORMAT=text
PROXY_ACCESS_LOG_PATH=/var/log/proxy/access.jsonl
PROXY_UPSTREAM_SWIFT=https://tuist.dev/api/registry/swift
Configuration File
listen: ":8080"
base_url: "http://localhost:8080"
storage:
url: "file:///var/cache/proxy/artifacts"
max_size: "10GB" # Optional: evict LRU when exceeded
retention:
default: "30d" # Optional: evict unused artifacts after 30 days; excludes generic
database:
driver: "sqlite"
path: "/var/lib/proxy/cache.db"
log:
level: "info"
format: "text"
access_log:
path: "/var/log/proxy/access.jsonl" # Optional JSONL activity log
Optional: override upstream URLs
upstream:
npm: "https://registry.npmjs.org"
cargo: "https://index.crates.io"
swift: "https://tuist.dev/api/registry/swift"
Optional: version cooldown (see above)
cooldown:
default: "3d"
See the configuration reference for every upstream key, environment variable, and default URL.
Run with config file:
./proxy -config /etc/proxy/config.yaml
PostgreSQL
SQLite is the default and works well for single-node deployments, with the database file on a local disk or block-backed volume. For multi-node setups, when only network filesystem storage such as NFS is available, or if you prefer a managed database, switch to Postgres:
database:
driver: "postgres"
url: "postgres://user:password@localhost:5432/proxy?sslmode=disable"
Or via environment variables:
PROXY_DATABASE_DRIVER=postgres
PROXY_DATABASE_URL=postgres://user:password@localhost:5432/proxy?sslmode=disable
The proxy creates tables automatically on first run.
S3 Storage
The proxy can store cached artifacts in S3 or any S3-compatible service (MinIO, R2, etc.) instead of the local filesystem.
storage:
url: "s3://my-bucket-name?region=us-east-1"
For S3-compatible services like MinIO:
storage:
url: "s3://my-bucket?endpoint=http://localhost:9000&disableSSL=true&s3ForcePathStyle=true"
Set credentials via standard AWS environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION).
Google Cloud Storage
The proxy can store cached artifacts in a GCS bucket using the gs:// URL scheme.
storage:
url: "gs://my-bucket-name"
Authentication uses Application Default Credentials, which means no credentials need to be embedded in the config or environment. Supported sources, in order:
- GKE Workload Identity — bind the Kubernetes service account running the proxy to a Google service account that has
roles/storage.objectAdminon the bucket. The proxy will use the workload's token automatically. - Attached service account on GCE, Cloud Run, Cloud Functions, etc.
GOOGLE_APPLICATION_CREDENTIALSenvironment variable pointing at a service account JSON key file.gcloud auth application-default loginfor local development.
GKE Workload Identity setup
# 1. Create a Google service account
gcloud iam service-accounts create git-pkgs-proxy \
--project=PROJECT_ID
2. Grant it access to the bucket
gsutil iam ch \
serviceAccount:git-pkgs-proxy@PROJECT_ID.iam.gserviceaccount.com:objectAdmin \
gs://my-bucket-name
3. Bind the Kubernetes service account to it
gcloud iam service-accounts add-iam-policy-binding \
git-pkgs-proxy@PROJECT_ID.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"
4. Annotate the Kubernetes service account
kubectl annotate serviceaccount KSA_NAME \
--namespace=NAMESPACE \
iam.gke.io/gcp-service-account=git-pkgs-proxy@PROJECT_ID.iam.gserviceaccount.com
Direct serve (signed URLs) with Workload Identity
When direct_serve: true is enabled, the proxy issues HTTP 302 redirects to presigned GCS URLs. Workload Identity provides no private key, so the GCS backend calls the IAM Credentials signBlob API. Grant the service account the token-creator role on itself:
gcloud iam service-accounts add-iam-policy-binding \
git-pkgs-proxy@PROJECT_ID.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator \
--member="serviceAccount:git-pkgs-proxy@PROJECT_ID.iam.gserviceaccount.com"
CLI Commands
serve (default)
Start the proxy server. This is the default command if none is specified.
proxy serve [flags]
proxy [flags] # same as 'proxy serve'
mirror
Pre-populate the cache from PURLs, SBOM files, or entire registries. Useful for ensuring offline availability or warming the cache before deployments.
# Mirror specific package versions
proxy mirror pkg:npm/[email protected] pkg:cargo/[email protected]
Mirror all versions of a package
proxy mirror pkg:npm/lodash
Mirror only the newest version of a package
proxy mirror --latest pkg:npm/lodash
Mirror from a CycloneDX or SPDX SBOM
proxy mirror --sbom sbom.cdx.json
Preview what would be mirrored
proxy mirror --dry-run pkg:npm/lodash
Control parallelism
proxy mirror --concurrency 8 pkg:npm/[email protected]
Mirror a container image for one platform
proxy mirror --platform linux/amd64 pkg:docker/library/[email protected]
Mirror a commit of an upstream.git repository into the git cache
proxy mirror pkg:git/github.com/git-pkgs/purl@0123456789abcdef0123456789abcdef01234567
The mirror command accepts the same storage and database flags as serve. Already-cached artifacts are skipped. If any package version fails to mirror, the command lists the errors and exits with status 1, so a CI step that warms the cache fails on a partial mirror.
With scanning.enabled, the mirror command refuses every package and container image so nothing is cached unscanned. Each one is reported as failed, so the command exits with status 1. Scanners fetch the staged bytes from the running server, which this command does not start. Use the mirror API below instead, since its jobs run inside the server and go through the pre-cache scan. --dry-run still lists what would be mirrored.
A mirror API is also available when the server is running:
# Start a mirror job
curl -X POST http://localhost:8080/api/mirror \
-H "Content-Type: application/json" \
-d '{"purls": ["pkg:npm/[email protected]"]}'
Start a mirror job from an inline CycloneDX or SPDX JSON SBOM
curl -X POST http://localhost:8080/api/mirror \
-H "Content-Type: application/json" \
-d '{"sbom":{"bomFormat":"CycloneDX","components":[{"purl":"pkg:npm/[email protected]"}]}}'
Check job status
curl http://localhost:8080/api/mirror/mirror-1
Cancel a running job
curl -X DELETE http://localhost:8080/api/mirror/mirror-1
hosted
Push package archives to a running proxy with hosted.enabled, list them and delete them. See Hosted Packages.
export PROXY_URL=https://proxy.example.com PROXY_HOSTED_TOKEN=...
proxy hosted push npm request 2.88.3 ./request-2.88.3.tgz
proxy hosted push composer monolog/monolog 3.5.0 ./monolog-3.5.0.zip
proxy hosted list npm request
proxy hosted delete npm request 2.88.3
stats
Show cache statistics without running the server.
# Text output
proxy stats
JSON output
proxy stats -json
Custom database path
proxy stats -database-path /var/lib/proxy/cache.db
With PostgreSQL
proxy stats -database-driver postgres -database-url postgres://user:pass@localhost/proxy
Show top 20 most popular packages
proxy stats -popular 20
Example output:
Cache Statistics
================
Packages: 45
Versions: 128
Artifacts: 128
Total size: 892.4 MB
Total hits: 1547
Packages by ecosystem:
npm 32
cargo 13
Most popular packages:
1. npm/lodash (342 hits, 24.7 KB)
2. npm/react (198 hits, 89.3 KB)
3. cargo/serde (156 hits, 234.1 KB)
Recently cached:
npm/[email protected] (2024-01-15 14:32, 54.2 KB)
cargo/[email protected] (2024-01-15 14:28, 412.8 KB)
API Endpoints
Registry Protocols
| Endpoint | Description |
|----------|-------------|
| GET / | Dashboard (web UI) |
| GET /health | Health check and upstream circuit breaker state (JSON; HTTP 200 healthy, 503 unhealthy) |
| GET /stats | Cache statistics (JSON) |
| GET /metrics | Prometheus metrics |
| GET /npm/* | npm registry protocol |
| POST /npm/-/npm/v1/security/* | npm/pnpm/Yarn audit endpoints, passed through to upstream |
| GET /npm/-/npm/v1/keys | npm registry signing keys, passed through to upstream |
| GET /npm/-/package/{name}/dist-tags | npm dist-tag ls, answered from the packument the proxy serves (cooldown and pushed versions applied); changing tags is not supported |
| GET /npm/-/npm/v1/attestations/{name}@{version} | npm provenance attestations of a version, fetched from the package's registry and cached |
| GET /npm/-/v1/search, GET /npm/-/ping | npm search and npm ping, passed through to upstream uncached |
| GET /cargo/* | Cargo sparse index protocol |
| GET /gem/* | RubyGems protocol |
| GET /go/* | Go module proxy protocol |
| GET /hex/* | Hex.pm protocol |
| GET /pub/* | pub.dev protocol |
| GET /pypi/* | PyPI simple/JSON API |
| GET /maven/* | Maven repository protocol |
| GET /nuget/* | NuGet V3 API |
| GET /composer/* | Composer/Packagist protocol |
| GET, POST /composer/api/security-advisories/ | Composer security advisories, passed through to upstream without private package names |
| GET /composer/lists/summary.json | Composer filter list summary, passed through to upstream uncached |
| GET /conan/* | Conan C/C++ protocol |
| GET /chef/* | Chef Supermarket protocol (universe, cookbook API and downloads) |
| GET /conda/* | Conda/Anaconda protocol |
| GET /cran/* | CRAN (R) protocol |
| GET /julia/* | Julia Pkg server protocol |
| GET /swift/* | Swift Package Registry v1 protocol |
| GET /helm/{repository}/* | HTTP Helm chart repository protocol |
| GET /homebrew/* | Homebrew JSON API |
| GET /v2/* | OCI/Docker registry protocol |
| GET /v2/homebrew/core/* | Homebrew core bottle manifests and blobs from GHCR |
| GET /apk/{repository}/* | Alpine APK repository protocol |
| GET /arch/{repository}/* | Arch Linux pacman repository protocol |
| GET /freebsd/{repository}/* | FreeBSD pkg repository protocol |
| GET /generic/{name}/* | Generic HTTP download proxy (GitHub release assets, mise/aqua) |
| GET /git/{name}/{repo}/info/refs, POST /git/{name}/{repo}/git-upload-pack | Git smart HTTP protocol, read-only |
| GET /debian/* | Debian/APT repository protocol (main archive) |
| GET /debian/{repository}/* | Debian/APT repository protocol (named archive, e.g. security) |
| GET /rpm/* | RPM/Yum repository protocol |
Mirror API
| Endpoint | Description |
|----------|-------------|
| POST /api/mirror | Start a mirror job (JSON body with purls or an inline sbom) |
| GET /api/mirror/{id} | Get job status and progress |
| DELETE /api/mirror/{id} | Cancel a running job |
Hosted API
Registered when hosted.enabled is set. Pushing and deleting require Authorization: Bearer .
| Endpoint | Description |
|----------|-------------|
| PUT /api/hosted/{ecosystem}/{name}/{version} | Push the request body as the archive for a version (npm and composer) |
| GET /api/hosted/{ecosystem}/{name} | List the archives pushed for a package |
| DELETE /api/hosted/{ecosystem}/{name}/{version} | Delete a pushed version |
Projects API
Registered when projects.enabled is set. Uploading and deleting require Authorization: Bearer . See Project inventory.
| Endpoint | Description |
|----------|-------------|
| PUT /api/projects/{project}/sbom | Replace a project's recorded package versions with the ones the SBOM in the body lists |
| GET /api/projects | List projects, or with ?purl=pkg:npm/[email protected] the projects using a package or version |
| GET /api/projects/{project} | A project's recorded package versions |
| DELETE /api/projects/{project} | Delete a project |
Cache API
Registered when cache_delete is set. It has no authentication of its own.
| Endpoint | Description |
|----------|-------------|
| DELETE /api/cache/{ecosystem}/{name}/{version} | Delete a version's cached files, or only ?filename= |
Enrichment API
The proxy provides REST endpoints for package metadata enrichment, vulnerability scanning, and outdated detection.
| Endpoint | Description |
|----------|-------------|
| GET /api/package/{ecosystem}/{name} | Get package metadata |
| GET /api/package/{ecosystem}/{name}/{version} | Get version metadata with vulnerabilities |
| GET /api/vulns/{ecosystem}/{name} | Get all vulnerabilities for a package |
| GET /api/vulns/{ecosystem}/{name}/{version} | Get vulnerabilities for a specific version |
| POST /api/outdated | Check multiple packages for outdated versions |
| POST /api/bulk | Bulk package metadata lookup |
Get Package Metadata
curl http://localhost:8080/api/package/npm/lodash
Response:
{
"ecosystem": "npm",
"name": "lodash",
"latest_version": "4.17.21",
"license": "MIT",
"license_category": "permissive",
"description": "Lodash modular utilities",
"homepage": "https://lodash.com/",
"repository": "https://github.com/lodash/lodash",
"registry_url": "https://registry.npmjs.org"
}
Get Version with Vulnerabilities
curl http://localhost:8080/api/package/npm/lodash/4.17.0
Response:
{
"package": {
"ecosystem": "npm",
"name": "lodash",
"latest_version": "4.17.21",
"license": "MIT",
"license_category": "permissive"
},
"version": {
"ecosystem": "npm",
"name": "lodash",
"version": "4.17.0",
"license": "MIT",
"published_at": "2016-06-17T03:59:56Z",
"yanked": false,
"is_outdated": true
},
"vulnerabilities": [
{
"id": "GHSA-p6mc-m468-83gw",
"summary": "Prototype Pollution in lodash",
"severity": "HIGH",
"cvss_score": 7.4,
"fixed_version": "4.17.12"
}
],
"is_outdated": true,
"license_category": "permissive"
}
Check Outdated Packages
```bash curl -X POST http://localhost:8080/api/outdated \ -H "Content-Type: application/json" \ -d '{ "packages": [ {"ecosystem": "npm", "name": "lodash", "version": "4.17.0
... (README truncated for length)