Documentation

Configuration

peryx reads one TOML file, passed with --config <path>. A few operational settings double as flags or PERYX_* environment variables, which override the file. Precedence is defaults < TOML file < environment < flags.

Top level

SettingFlagEnvironmentTOML keyDefault
Bind host--hostPERYX_HOSThost127.0.0.1
Bind port--portPERYX_PORTport4433
Data directory--data-dirPERYX_DATA_DIRdata_dirperyx-data
Offline mode--offlinePERYX_OFFLINEofflinefalse
Config file--config / -c(n/a)(n/a)(none)
Cache freshness (seconds)(file/env only)PERYX_CACHE_TTL_SECScache_ttl_secs300
Indexes(file only)(n/a)[[index]](see below)
Rate limits(file only)(n/a)[rate_limit](see below)

Environment variables sit between the file and flags: a PERYX_* value overrides the TOML file, and a flag overrides the variable. Only scalar settings are environment-configurable. The [[index]] topology and [rate_limit] block stay file-only, since neither maps to a flat variable. An empty variable is treated as unset. The [log] block also reads variables (PERYX_LOG_LEVEL, PERYX_LOG_FORMAT, PERYX_LOG_SINK, PERYX_LOG_FILE); see [log].

cache_ttl_secs is a fallback: when an upstream response carries a usable Cache-Control lifetime (s-maxage or max-age), that lifetime governs the page instead. The fallback applies when the header is absent, no-cache/no-store, or zero. Artifacts never expire; they are content-addressed by sha256, so a changed upstream file is a new entry on the page rather than a mutation.

offline = true disables upstream network access for configured cached indexes. Whatever an ecosystem has cached serves from disk: PyPI project pages, PEP 658 metadata siblings, and wheels; OCI manifests and blobs. A cold cached-index miss returns 503; virtual-index routes still serve any hosted layer that can answer. Use peryx mirror sync before enabling offline mode on a machine that must run without network access.

TLS

peryx serves plain HTTP by default, which is the right choice for a laptop: pip/uv accept any URL, and docker/podman trust a loopback registry (localhost, 127.0.0.0/8) over HTTP with no configuration. To serve over the network, where clients demand HTTPS, turn on TLS with one of two mutually exclusive tables. Neither is set by default, and an unconfigured server keeps the plain-HTTP path.

A [tls] table serves HTTPS from a certificate and key you provide:

[tls]
cert = "/etc/peryx/fullchain.pem"
key = "/etc/peryx/privkey.pem"

An [acme] table obtains and renews a certificate from an ACME provider (Let's Encrypt), so a publicly reachable deployment serves trusted HTTPS with no manual certificate handling and no client-side insecure flag:

[acme]
domains = ["registry.example.com"]
contact = "admin@example.com"
cache-dir = "/var/lib/peryx/acme"  # where issued certificates are cached; default "acme-cache"
staging = false                    # true uses Let's Encrypt staging while testing
TableKeyMeaningDefault
[tls]certPEM certificate chain(required)
[tls]keyPEM private key(required)
[acme]domainsDomains to request a certificate for; reachable on port 443(required)
[acme]contactContact email the ACME account registers(required)
[acme]cache-dirWhere certificates and the account key are cachedacme-cache
[acme]stagingUse the provider's staging environmentfalse

For an [acme] deployment the domain's DNS must point at the server and port 443 must be reachable, since the ACME handshake happens there. Behind a load balancer or reverse proxy that already terminates TLS, leave both tables unset and let the proxy hold the certificate.

[[index]]

Each [[index]] table declares one index. name is required; exactly one of cached, hosted, or layers selects the role. peryx rejects unknown keys.

KeyRoleMeaningDefault
nameallIdentifier other indexes reference in layers(required)
routeallURL prefix the index is served undersame as name
ecosystemallPackaging format: pypi or ocipypi
cachedcachedUpstream URL to cache (a Simple index, or a /v2/ registry for OCI)
usernamecachedBasic-auth username for the upstream(none)
passwordcachedBasic-auth password for the upstream(none)
tokencachedBearer token; takes precedence over username/password(none)
upstream_concurrencycachedCap on concurrent upstream fetches; 0 is unlimited and the default0
offlinecachedServe this cached index from disk onlyfalse
prefetchcachedPackage and artifact selection for peryx mirror(see below)
hostedhostedtrue marks this index as a hosted store (implied by upload_token)false
upload_tokenhostedBasic-auth password uploads must present; unset disables uploads(none)
volatilehostedAllow delete and overwritetrue
layersvirtualOrdered index names to compose; first match per filename wins
uploadvirtualHosted layer that receives uploadsfirst hosted layer
policyallNested index policy tableempty
webhookallSigned delivery targets for upload and index-change eventsnone

A route is a raw URL path prefix. It must be one or more non-empty path segments separated by /; each segment may contain only ASCII letters, digits, -, ., _, and ~. Startup rejects routes with a leading or trailing /, empty segments, percent encoding, traversal segments, control characters, spaces, and routes whose first segment is reserved for Peryx endpoints such as browse, stats, +stats, +status, api-docs, metrics, and pkg.

Declaring any [[index]] replaces the default topology, which ships a trio per ecosystem: a cached upstream, a hosted store, and a virtual index that layers the two.

[[index]]
name = "pypi"
cached = "https://pypi.org/simple/"

[[index]]
name = "hosted"
hosted = true

[[index]]
name = "root/pypi"
layers = ["hosted", "pypi"]
upload = "hosted"

[[index]]
name = "dockerhub"
ecosystem = "oci"
cached = "https://registry-1.docker.io"

[[index]]
name = "images"
ecosystem = "oci"
hosted = true

[[index]]
name = "root/oci"
ecosystem = "oci"
layers = ["images", "dockerhub"]
upload = "images"

Startup rejects duplicate names, duplicate routes, invalid routes, layers entries that name no index, layers that mix ecosystems, and an upload target that is not a hosted index.

[index.policy]

Policy rules apply to the index that owns the table. A cached-index policy filters that cache; a hosted policy filters direct uploads and hosted-route reads; a virtual policy filters the merged index clients use. Project names are compared after PEP 503 normalization.

[[index]]
name = "root/pypi"
layers = ["hosted", "pypi"]
upload = "hosted"

[index.policy]
allow_projects = ["flask", "requests"]
block_projects = ["bad-package"]
allow_versions = ">=1,<3"
allow_package_types = ["wheel"]
block_package_types = ["sdist"]
allow_wheel_pythons = ["py3", "cp313"]
block_wheel_platforms = ["win_amd64"]
max_file_size_bytes = 104857600
max_project_size_bytes = 1073741824
KeyMeaning
allow_projectsOnly these normalized projects may be served, mirrored, or uploaded
block_projectsThese normalized projects are denied
allow_versionsPEP 440 specifier set accepted for parsed distribution filenames
allow_package_typesAccepted parsed file types: wheel, sdist
block_package_typesDenied parsed file types: wheel, sdist
allow_wheel_pythonsAccepted wheel Python tags, matched against each dot-compressed tag segment
block_wheel_pythonsDenied wheel Python tags
allow_wheel_platformsAccepted wheel platform tags, matched against each dot-compressed tag segment
block_wheel_platformsDenied wheel platform tags
max_file_size_bytesMaximum file size from the Simple API size field or from an uploaded file
max_project_size_bytesMaximum sum of retained file sizes for one project detail page

File and project size rules require declared sizes. A file without size is denied by max_file_size_bytes; a project page with any retained file lacking size is denied by max_project_size_bytes. Active policies use the buffered Simple-page path so file lists and PEP 691 versions are filtered together before peryx serves bytes.

allow_projects, block_projects, max_file_size_bytes, and max_project_size_bytes are ecosystem-neutral and apply to an OCI index too, matching on image name and blob size: a blocked image is hidden on reads and refused on push, and a layer or manifest over the size limit is refused. The rest of the keys above cover version specifiers, package types, and wheel tags. These are Python-specific (PEP 440 versions, wheel/sdist types, wheel tags) and have no OCI counterpart, so they are implemented in the PyPI ecosystem crate and apply only to a PyPI index. Each ecosystem contributes its own matchers to the same neutral [index.policy] engine through a rule trait.

[index.prefetch]

Cached indexes can declare the default selection for peryx mirror plan, peryx mirror sync, and peryx mirror verify. CLI flags add package selectors and override booleans or mode for one run.

[[index]]
name = "pypi"
cached = "https://pypi.org/simple/"

[index.prefetch]
mode = "selected"
packages = ["requests>=2,<3"]
requirements = ["requirements.txt"]
include_wheels = true
include_sdists = true
python_tags = ["py3", "cp312"]
abi_tags = ["none", "abi3"]
platform_tags = ["any", "manylinux_2_28_x86_64"]
max_file_size_bytes = 524288000
metadata_only = false
KeyValuesDefault
modeselected, all, metadata-onlyselected
packagespackage selectors such as flask>=3[]
requirementsrequirements or constraints files[]
include_wheelsbooleantrue
include_sdistsbooleantrue
python_tagswheel Python tags[]
abi_tagswheel ABI tags[]
platform_tagswheel platform tags[]
max_file_size_bytespositive integer(none)
metadata_onlybooleanfalse

mode = "all" reads the upstream root Simple project list and then visits matching projects. Artifact filters apply after a project page is fetched. mode = "metadata-only" implies metadata_only = true.

The wheel/sdist and wheel-tag keys above are the PyPI selection set and seed peryx mirror for a PyPI index. For an OCI index, packages is the image list peryx mirror pulls (image references such as library/alpine:3.19), the same way it seeds a PyPI index's project list; --image <ref> on the command line adds one-off references on top. The PyPI wheel/sdist/wheel-tag keys do not apply to an OCI index.

[rate_limit]

Rate limits are local to one peryx process and disabled by default. When enabled = true, they use fixed windows and bounded in-memory buckets; restarting the process clears the buckets. max_clients caps the number of client/class buckets kept in memory. Set a class requests or window_secs to 0 to disable that class limit.

For authenticated requests, peryx hashes the Authorization header and uses the hash as the bucket key. It does not store the credential value. Other requests use the peer IP address. In in-process tests and deployments without socket peer metadata, peryx falls back to X-Forwarded-For, then X-Real-IP, then 127.0.0.1.

KeyMeaningDefault
enabledInstall the HTTP request limiterfalse
max_clientsMaximum client/class buckets kept in memory8192

Each route class is a sub-table with requests and window_secs:

TableRoute classDefault
[rate_limit.listing]Project listing and detail pages600 / 60s
[rate_limit.metadata]PEP 658/714 .metadata siblings1200 / 60s
[rate_limit.artifact]Artifact downloads and archive inspection300 / 60s
[rate_limit.upload]Upload, yank, restore, and delete requests60 / 60s
[rate_limit.admin]Status, stats, metrics, and discovery endpoints120 / 60s

Example:

[rate_limit]
enabled = true
max_clients = 4096

[rate_limit.listing]
requests = 300
window_secs = 60

[[index]]
name = "pypi"
cached = "https://pypi.org/simple/"
upstream_concurrency = 4

[[index.webhook]]

Put webhook tables under the index that should emit them. A target on a virtual index receives events for requests made through the virtual-index route; the payload also names the hosted layer that stored the change.

[[index]]
name = "root/pypi"
layers = ["hosted", "pypi"]
upload = "hosted"

[[index.webhook]]
name = "ci"
url = "https://ci.example/hooks/peryx"
secret_env = "PERYX_WEBHOOK_SECRET"
events = ["upload", "delete", "restore"]
KeyMeaningDefault
nameStable target name used in delivery logs
urlHTTP or HTTPS endpoint that receives JSON payloads; credentials, query, and fragment are rejected
secretLiteral HMAC signing secret
secret_envEnvironment variable that contains the HMAC signing secret
eventsEvent names to send; omit or leave empty for all supported event namesall

Use one of secret or secret_env. Supported event names are upload, yank, unyank, delete, restore, promote, project-status, and management. Peryx emits upload, yank, unyank, delete, and restore from the write endpoints in this release; the other names reserve the contract for management surfaces that use this runtime.

Peryx stores pending deliveries in the metadata database and sends them outside the request path. A failed delivery retries up to five attempts with capped backoff of 5, 15, 45, and 135 seconds. The delivery log stores the payload, target name, attempt count, next retry time, response status, and last error. It does not store webhook secrets.

[log]

KeyValuesDefault
levela tracing directive: error ... trace, per-module filtersinfo
formatpretty, jsonpretty
sinkstdout, file, journald, syslogstdout
filepath, required when sink = "file"(none)

The flags --log-level, --log-format, --log-sink, --log-file, -v, and -vv override these, as do the PERYX_LOG_LEVEL, PERYX_LOG_FORMAT, PERYX_LOG_SINK, and PERYX_LOG_FILE variables (below the flags in precedence).

On this page