@pdsjs/oci #
Host OCI container images natively in an atproto account. Tags, manifests
and layers live in the account's PDS as a dev.pdsjs.oci.repo record plus
blob storage, so an image rides along in CAR backups, migrates with the
account, and needs no separate registry service:
docker login pds.example.com -u alice.example.com
docker push pds.example.com/my-app:v1
docker pull pds.example.com/my-app:v1 # anonymous
How it works #
- An OCI digest is
sha256:<hex>; an atproto blob CID is CIDv1 + raw codec- a sha256 multihash. Both hold the same 32 hash bytes, so a layer is one
blob and a blob fetch is a digest re-encoding plus
getBlob. No mapping table exists.
- a sha256 multihash. Both hold the same 32 hash bytes, so a layer is one
blob and a blob fetch is a digest re-encoding plus
- Manifest bytes are stored as blobs exactly as pushed, because a digest
covers the exact serialization. The record carries each manifest's media
type so pulls are served with the right
Content-Type. - One record per image repository (collection
dev.pdsjs.oci.repo, rkey = name with/encoded as:). It holds the tag list and a blob reference for every stored manifest, config and layer, which keeps those blobs out of the PDS's orphan cleanup. Deleting a manifest drops the references and the orphan sweep frees the bytes. - Layer uploads stream through the blob writer with an incremental hash, so a layer is never held in memory whole. Monolithic and chunked uploads, cross-repository mounts, ranged blob reads and multi-arch indexes all work.
Authentication #
Pulls are anonymous. Pushes authenticate with the account password or an
app password, sent the way docker login sends them; the username is the
account's handle or DID. The server issues Bearer tokens from
/v2/token — the flow docker requires for anonymous pulls — and also
accepts Basic credentials directly, so curl -u works.
Enabling #
Node:
const server = await createServer({
// ...
experimental: { oci: true },
});
Cloudflare: set PDS_EXPERIMENTAL_OCI = "true".
Both mount createOciExtension() through core's extensions option. A
platform that builds PersonalDataServer itself does the same:
import { createOciExtension } from '@pdsjs/oci/extension';
new PersonalDataServer({
// ...
extensions: [createOciExtension({ maxBlobSize: 512 * 1024 * 1024 })],
});
The registry serves under /v2/ on the PDS hostname. Docker requires TLS
for any registry that is not localhost; a PDS behind HTTPS already
satisfies that.
Limits #
- Layers stream, but each layer arrives in one request unless the client chunks. A platform's per-request body cap (100 MB on the lowest Cloudflare plan) bounds the largest layer such a deployment accepts.
- The referrers API is not served; clients fall back to the tag schema.
_cataloglists the first hundred repositories.