Documentation

Work with registry behavior

The examples proxy an upstream that advertises non-sha256 manifest digests and recover an interrupted blob upload. They use peryx at http://127.0.0.1:4433.

Front a registry that uses non-sha256 digests

Most registries content-address with sha256, but the OCI spec allows sha512 and other registered algorithms, and some registries advertise their Docker-Content-Digest in one of them. peryx proxies such an upstream with no special configuration; the two things to get right are which digest to pin by and where the support stops.

Point a cached index at it

Digest handling requires no index setting. Configure a cached index:

# peryx.toml
[[index]]
name = "reg"
route = "reg"
ecosystem = "oci"

[[index.upstream]]
name = "primary"
url = "https://registry.example.com"

Pull through the proxy. peryx fetches the manifest, hashes the exact bytes under its own sha256, and serves them:

crane manifest --insecure 127.0.0.1:4433/reg/team/app:1.0

Pin the canonical digest

peryx addresses every manifest it stores by sha256, so the digest it hands your clients for a tag pull is a sha256: value, even when the upstream advertised sha512. Read it from the response header:

curl -sI http://127.0.0.1:4433/v2/reg/team/app/manifests/1.0 | grep -i docker-content-digest

Pin deployments to that sha256. It is the digest peryx serves the image under and the one a client verifies the bytes against. If you carry an upstream sha512 digest from elsewhere, a pull by it still works, and peryx serves the bytes under the digest you request and echoes it back:

crane manifest --insecure 127.0.0.1:4433/reg/team/app@sha512:<hex>

sha256-only operations

The broader digest support applies to manifest reads through a proxy. These operations require sha256:

  • Blobs. A blob pull, mount, or upload commit must use sha256:; any other algorithm answers 400 DIGEST_INVALID with only sha256 blob digests are supported. Peryx rejects a blob pushed under a non-sha256 digest.
  • A wrong sha256 advertisement. If the upstream advertises a sha256: digest that does not hash the bytes it sent, peryx returns 502 and caches nothing.
  • Offline mirror pins. A mirror entry pinned by digest must be sha256:. repo@sha512:… fails the mirror's own sha256 comparison; mirror by tag instead, which stores under the canonical sha256.

Verify the proxy

Confirm a tag pull succeeds and reports a sha256 digest:

curl -si http://127.0.0.1:4433/v2/reg/team/app/manifests/1.0 | head -3

A 200 with docker-content-digest: sha256:... confirms the proxy response. A 502 means the upstream advertised a sha256: digest that did not match its bytes. The exact rules are in content digest algorithms, and the reasoning in why peryx accepts a non-sha256 content digest.

Mount a blob from another repository

Use a cross-repository mount when you reuse a layer from another repository. Include the peryx index route in both names. Read the digest from the source manifest, then request the mount with that full source name:

curl -sS -i -u _:<token> -X POST \
  "http://127.0.0.1:4433/v2/images/target/app/blobs/uploads/?mount=sha256:<hex>&from=images/source/app"

peryx returns 201 Created after it links the target without transferring a layer body. peryx opens the upload session in Location with 202 Accepted when the source lacks the digest or the bytes. It takes the same path without from. You need pull access for a private source; peryx returns the source repository's 401 challenge when the credential may push the target but cannot pull images/source/app.

Reclaim unreferenced blob bytes

peryx removes the target repository link for DELETE /v2/<name>/blobs/<digest> and returns 202 Accepted. peryx keeps the content-addressed bytes so another repository or ecosystem can use the same digest. After deleting the related manifests and blob links, inspect the orphan candidates before unlinking them:

peryx cache purge orphaned-blobs --data-dir /var/lib/peryx
peryx cache purge orphaned-blobs --data-dir /var/lib/peryx --yes

The collector asks each registered blob-reference provider for references and retains a digest while any provider reports it.

Cancel an in-progress upload

A container push is a series of blob uploads. A client can crash mid-layer, or a chunk can arrive out of order and make peryx answer 416. Both cleanups act on an upload session, so both need the hosted index's access-token secret as the Basic-auth password (-u _:<token>).

The metadata store contains each open session record, and the filesystem contains its staged bytes. After a restart, peryx reads both. Send a status GET, then resume at the offset in its Range header. An unfinished session remains until DELETE, a size rejection, or idle reclamation. peryx marks it eligible after one hour without a status request or PATCH attempt. By default, a local worker runs the pass once per minute. To release the staged bytes before the timeout, DELETE the session URL, the Location peryx returned when the session opened:

curl -sS -i -u _:<token> -X DELETE \
  http://127.0.0.1:4433/v2/images/<repo>/blobs/uploads/<session>
# 204 No Content

204 means the session and its staged bytes are gone. A session id peryx does not know, including one you already finished or cancelled, answers 404 BLOB_UPLOAD_UNKNOWN:

curl -sS -i -u _:<token> -X DELETE \
  http://127.0.0.1:4433/v2/images/<repo>/blobs/uploads/<already-gone>
# 404 Not Found

A 403 DENIED caused by max_artifact_size_bytes also removes the session. Do not send DELETE or resume from the last range; reduce the blob or raise the index limit, then start another upload.

Send follow-up requests to the exact Location from the opening response. peryx returns 404 BLOB_UPLOAD_UNKNOWN when the repository path differs, including when the caller can write both repositories.

peryx does not discard an open session on restart. Cancel it when a CI job aborts or a script abandons an upload to release its staged bytes before expiry.

Resume a push that got a 416

peryx answers a PATCH whose Content-Range does not begin where the last chunk ended with 416 Range Not Satisfiable, and keeps the bytes it already has. The 416 reports the session coordinates you need to continue:

416 Range Not Satisfiable
Location: /v2/images/<repo>/blobs/uploads/<session>
Docker-Upload-UUID: <session>
Range: 0-<end>

Read Range: 0-<end>: it is the byte span peryx holds, so the next chunk must start at byte <end> + 1. Re-send the chunk from there against the Location URL:

curl -sS -i -u _:<token> -X PATCH \
  -H 'Content-Type: application/octet-stream' \
  -H 'Content-Range: <end+1>-<new-end>' \
  --data-binary @chunk \
  http://127.0.0.1:4433/v2/images/<repo>/blobs/uploads/<session>
# 202 Accepted, Range: 0-<new-end>

If you have lost track of how much landed, query the session. GET on the session URL reports progress as Range: 0-<end> without changing anything, so you can read the offset before you resume:

curl -sS -i -u _:<token> \
  http://127.0.0.1:4433/v2/images/<repo>/blobs/uploads/<session>
# 204 No Content, Range: 0-<end>

Then finish the push with PUT …?digest=sha256:<hex> once the last chunk is in. docker, podman, and crane run this recovery for you; you only drive it by hand when you are scripting an upload or debugging one that stalls, as in push a blob chunk by chunk.

On this page