Profile
Back to NewsBack
GitHub Trending 35 min
Reader Mode
git-pkgs/proxy: A lightweight caching proxy for package registries.

git-pkgs/proxy: A lightweight caching proxy for package registries.

11 hours ago

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
Or replace 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 that serves HTTPS:

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:

  1. Command line flags (highest priority)
  2. Environment variables
  3. 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.objectAdmin on the bucket. The proxy will use the workload's token automatically.
  • Attached service account on GCE, Cloud Run, Cloud Functions, etc.
  • GOOGLE_APPLICATION_CREDENTIALS environment variable pointing at a service account JSON key file.
  • gcloud auth application-default login for 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)

Chat with me