Documentation

Serve images air-gapped

A network with no route to Docker Hub can still pull public images through peryx, as long as the images land in peryx's content-addressed blob store before the gap closes. Two topologies cover the common cases: a cache pinned offline, or a data directory carried across. Images your team pushes directly to a hosted store need no upstream at all.

Pre-seed the cache on a connected machine

On a machine that can still reach Docker Hub, run peryx with a cached proxy and mirror every image the air-gapped side will need. peryx mirror sync pulls each image's manifest and every blob it names (following a manifest list into its per-platform manifests), running the upstream bearer-token handshake and verifying each blob against its digest:

# peryx.toml on the connected machine
host = "127.0.0.1"
port = 4433
data_dir = "./peryx-data"

[[index]] # cached: read-through cache of Docker Hub
name = "dockerhub"
route = "dockerhub"
ecosystem = "oci"
cached = "https://registry-1.docker.io"
peryx mirror sync dockerhub \
  --image library/alpine:latest \
  --image library/nginx:1.27 \
  --image library/python:3.13-slim

Every manifest, config blob, and layer blob now sits under ./peryx-data, deduplicated by digest. Re-run the command whenever the image set changes; peryx mirror verify dockerhub --image … checks that the store still holds a complete image. A running server is not required; the command reads the config and writes the data directory directly.

Approach one: pin the cache offline

Set offline = true on the cached index and peryx never reaches upstream. Everything already in the blob store serves from disk; a pull of something that was not pre-seeded returns an error rather than a network fetch:

# peryx.toml on the air-gapped machine
host = "0.0.0.0"
port = 4433
data_dir = "./peryx-data"

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

This fits a machine that was connected during the pre-seed and later lost its route: same data directory, one flag flipped. Clients pull through the dockerhub route exactly as before.

Approach two: carry the data directory across the gap

When the air-gapped machine was never connected, move the store to it. Pre-seed on the connected machine as above, then carry ./peryx-data (the blob store and its metadata) across the gap and run peryx there. A backup keeps the copy consistent:

# connected machine
peryx backup create --data-dir ./peryx-data ./peryx-backup
peryx backup verify ./peryx-backup

# air-gapped machine
peryx restore ./peryx-backup --data-dir ./peryx-data
peryx serve --config peryx.toml

The air-gapped machine's config declares the same cached index with offline = true, so a pull of a pre-seeded image serves from the carried-over store and a cold miss returns an error instead of reaching for a network that is not there.

Hosted images need no upstream

An image your team builds and pushes to a hosted index never involves Docker Hub, so it works air-gapped with no pre-seed at all. Declare a hosted store alongside the cache:

[[index]] # hosted: your own images, push needs the token
name = "team"
route = "team"
ecosystem = "oci"
hosted = true
upload_token = "team-secret"

Push and pull it directly on the air-gapped side:

docker login 127.0.0.1:4433 -u _ -p team-secret
docker tag my-app 127.0.0.1:4433/team/my-app:1.0
docker push 127.0.0.1:4433/team/my-app:1.0
docker pull 127.0.0.1:4433/team/my-app:1.0

podman and crane push the same way with their insecure-transport flags. To serve hosted images and pre-seeded upstream ones under one route, front both with a virtual index; see build a team registry.

What to check

  • curl http://<host>:4433/+status lists the indexes and their counters.
  • curl http://<host>:4433/+status | jq '.indexes[].upstream?.offline' shows which cached indexes run offline.
  • A pull that errors from a cached route means a client asked for an image the offline store does not hold; add it to the pre-seed loop and repeat the carry.
On this page