Documentation

From devpi

devpi is the long-standing Python answer to this problem: a caching pypi.org mirror plus user-owned indexes with inheritance, a web UI, primary/replica replication, and a pluggy-based plugin ecosystem. It runs as a Pyramid application on an embedded waitress server, keeps its state in an SQLite key-value store with release files on the filesystem, and expects an nginx and supervisor front for production (its own devpi-gen-config generates those files). velodex covers the same read-through core in one static Rust binary.

Comparison against velodex

Overlap

Both are read-through pypi.org mirrors that cache what they fetch, host private uploads, and let hosted names shadow the cached index. For a caching mirror the two overlap almost completely:

  • Read-through mirroring of pypi.org (or any simple index), cached on first use.
  • Private uploads over the twine API, served from the same host as the cached index.
  • Composition: devpi's index inheritance (bases) maps onto velodex's virtual indexes, and hosted files shadow upstream ones in both.
  • Yank and delete of hosted files.
  • A web UI for browsing packages (devpi-web; built into velodex at /).
  • Streaming artifact downloads: devpi's FileStreamer and velodex both tee a wheel to disk while the client reads it, and both address stored files by sha256.

Extra: what devpi does that velodex does not

Migrating to velodex means giving these up:

  • Users and per-index ACLs. devpi indexes belong to users, each with its own acl_upload. velodex has one upload token per hosted index and open reads.
  • Replication. devpi's primary/replica protocol streams a changelog to read-only replicas. velodex has no equivalent; you run one instance per site and let each warm itself.
  • Promotion (push). devpi can promote a release from one index to another server-side. In velodex that is a re-upload.
  • A plugin ecosystem. devpi-ldap, devpi-lockdown, and friends hook devpi through pluggy. velodex's extension points are its HTTP API and its configuration, nothing loadable.

Missing: what velodex adds

  • PEP 658 metadata by default. devpi 6.x ships core-metadata as experimental, behind --enable-core-metadata. velodex serves it out of the box and synthesizes it with HTTP byte-range reads when an upstream lacks it, so resolution can beat the upstream once metadata is cached.
  • Correctness under a concurrent cold burst. On the first parallel fetch of a project, devpi can evaluate the request against an as-yet-empty project list, return a 404, and cache that "does not exist" for its 30-minute mirror window; uv then fails the install. velodex single-flights concurrent misses onto one upstream fetch, so ten cold installs of the same project all succeed.
  • Built-in observability. Prometheus metrics and per-file usage counters are part of the server, not plugin territory.
  • One process. A single static binary with no nginx or supervisor front, no devpi-init step, and no external database.

Performance vs velodex

The benchmark suite runs both servers from their published packages against the same workload. Cold and warm installs through uv:

uv: install the top 51 PyPI packages (ratios vs direct)
velodex devpi
cold cache 3.8 s (1.14x) 13.8 s (4.12x)
warm cache 2.7 s (0.83x) 6.0 s (1.86x)
server CPU 1.0 s (1.00x) 15.2 s (14.52x)
server peak memory 537 MB (1.00x) 1,190 MB (2.21x)

The parallel-install workload is where the concurrency difference shows up: ten virtualenvs install the same project at once, each with an empty client cache.

uv: ten venvs install polars at once (ratios vs direct)
velodex devpi
cold cache: 10 parallel installs 1.6 s (0.33x) error (n/a)
warm cache: 10 parallel installs 1.5 s (0.21x) error (n/a)
server CPU 545 ms (1.00x) 0 ms (0.00x)
server peak memory 122 MB (1.00x) 1,125 MB (9.24x)

The request workload drives a swarm of resolvers reading full project pages:

simple-page requests against a warm cache (ratios vs direct)
velodex devpi
1 user: requests/s 681 req/s (2.95x) 79 req/s (0.34x)
1 user: p95 latency 6 ms (0.62x) 45 ms (5.10x)
32 users: requests/s 2,987 req/s (3.32x) 76 req/s (0.08x)
32 users: p95 latency 24 ms (0.20x) 632 ms (5.22x)
server CPU 2m 49.4s (1.00x) 38.6 s (0.23x)
server peak memory 324 MB (1.00x) 614 MB (1.89x)

How to migrate

devpi's mirror state does not migrate and does not need to: velodex's cache refills on first use. Only your uploaded packages need a twine upload pass into the new hosted index. Map the commands and knobs across:

devpivelodex
devpi-init then devpi-server --port 3141velodex serve (no init step)
http://host:3141/{user}/{index}/+simple/http://host:4433/{route}/simple/
devpi index -c dev bases=root/pypia virtual index with layers = ["dev-hosted", "pypi"] in TOML
devpi login + devpi uploadtwine upload --repository-url http://host:4433/{route}/ dist/* (any username, upload_token as password)
devpi remove pkg==1.0DELETE /{route}/{project}/{version}/ (removal guide)
volatile=Falsevolatile = false on the hosted index
mirror_whitelistnot needed: hosted names shadow the cached index by default (why)
acl_uploadone upload_token per hosted index
devpi-web pluginbuilt in at /

Gotchas

  • One upload token replaces per-person write control. If you relied on distinct acl_upload per user, issue a distinct hosted index (and token) per team instead.
  • No push between indexes. Promoting a release is a re-upload into the target index.
  • Plugin hooks have no counterpart. Anything you drove through devpi-ldap or devpi-lockdown moves to a layer in front of velodex or into your own automation against its HTTP API.
  • Replica topologies collapse to one instance per site. There is no changelog to follow; each velodex warms independently.
On this page