Documentation

Cache packages for CI

CI rebuilds environments from scratch, so every job downloads the same wheels again. Run one velodex where your runners live and point the installers at it; the first job warms the cache and the rest install from local disk.

Run velodex next to the runners

On the CI host, or as a service in the runner network:

velodex serve --host 0.0.0.0 --port 4433 --data-dir /var/lib/velodex

The data directory is the cache; give it a persistent volume. Nothing else is stateful.

In Kubernetes or docker-compose, the same thing is one container with one volume. The image only needs the binary and the data mount; there is no database or sidecar.

Point the installers at it

Installers pick up the index from an environment variable, so most setups change zero pipeline files: set it once at the runner or organization level:

export UV_INDEX_URL=http://velodex.internal:4433/root/pypi/simple/
export PIP_INDEX_URL=http://velodex.internal:4433/root/pypi/simple/
# pyproject.toml, for uv-managed projects
[[tool.uv.index]]
url = "http://velodex.internal:4433/root/pypi/simple/"
default = true

Jobs that already pass --index-url explicitly keep working; the flag and the variable point at the same place.

Docker builds

Builds inside docker build do not see the host network by default. Either pass the index through a build argument:

ARG PIP_INDEX_URL
RUN pip install -r requirements.txt
docker build --build-arg PIP_INDEX_URL=http://velodex.internal:4433/root/pypi/simple/ .

or run the build on a network where velodex.internal resolves (--network with BuildKit). BuildKit's own cache mounts still help per machine; velodex makes the cache shared across machines, tags, and projects.

Verify it is working

Watch a couple of jobs, then check what the cache absorbed:

curl -s 'http://velodex.internal:4433/+stats?index=root/pypi' | jq .totals

downloads and bytes count what velodex served; once the working set is warm, upstream traffic drops to page revalidations (refreshes, mostly 304s with no body). The dashboard shows the same numbers with per-project drill-down, and /metrics feeds Prometheus.

Why this works as well as it does

  • Wheels are immutable and content-addressed: each crosses your uplink once, ever (architecture).
  • Cold misses stream through at upstream speed, so the warm-up phase costs nothing extra (measurements).
  • A pypi.org outage stops being a build outage: pages serve stale, artifacts serve from disk.
On this page