Serve a restricted or air-gapped network
A full PyPI mirror consumes tens of terabytes. Most networks use a fraction of its packages. A read-through cache holds the packages that users request. Choose a controlled-egress or isolated topology.
Controlled egress: peryx as the choke point
The network allows outbound traffic from approved hosts. Run peryx on one of them. Other hosts install through peryx without an internet route:
# peryx.toml on the egress host
host = "0.0.0.0"
port = 4433
data_dir = "/var/lib/peryx"
Set PIP_INDEX_URL or UV_INDEX_URL to http://<egress-host>:4433/root/pypi/simple/. The egress host becomes the
firewall boundary and monitoring point. peryx counts downloads per project and file, and
the virtual route merges private and cached files. Add a
project-isolation policy for private names that must exclude
upstream versions.
If the egress host itself must go through a corporate proxy, standard HTTPS_PROXY environment variables apply to
peryx's upstream client.
True air gap: warm, carry, serve
With no route at all, populate the cache on a connected network and move the data directory across the gap. For a requirements-bounded mirror:
# connected side
peryx mirror plan root/pypi --data-dir ./peryx-data --option 'requirements=["requirements.txt"]'
peryx mirror sync root/pypi --data-dir ./peryx-data --option 'requirements=["requirements.txt"]'
peryx mirror verify root/pypi --data-dir ./peryx-data --option 'requirements=["requirements.txt"]'
peryx stores the selected pages, PEP 658 metadata, wheels, and sdists under
./peryx-data. Create and verify a backup, carry it across the gap, restore it, and enable offline mode:
# connected side
peryx backup create --data-dir ./peryx-data ./peryx-backup
peryx backup verify ./peryx-backup
# isolated side
peryx restore ./peryx-backup --data-dir ./peryx-data
peryx serve --data-dir ./peryx-data --offline
The backup includes the metadata store, a configuration snapshot, and the blob files referenced by metadata records. Offline mode blocks upstream requests. peryx serves artifacts and cached project pages from the store; requests for content absent from the backup return a resolver-visible miss. Repeat the cycle after the requirement set changes.
Resolve a lock file (uv.lock or requirements.txt with hashes) on the connected side. The isolated side will then
request content from that lock.
Use mode="all" for a full upstream walk:
peryx mirror sync pypi --data-dir ./peryx-data --option 'mode="all"'
peryx mirror verify pypi --data-dir ./peryx-data --option 'mode="all"'
Full PyPI consumes many terabytes. Set python_tags, abi_tags, platform_tags, and max_file_size_bytes to limit
the wheel set.
Verification
curl -u admin:"$ADMIN_PASSWORD" http://<host>:4433/+status | jq '.indexes[].upstream?.offline'shows which cached indexes run offline; the index topology needs an administrator credential.curl -u operator:"$OPERATOR_PASSWORD" 'http://<host>:4433/+stats?index=root/pypi'shows what the cache is serving.- A
503from a cached index route means a client asked for something the offline cache does not contain.