Small OCI registry proxy with content-addressed blob caching on local disk.
This project sits between clients (podman, regctl, etc.) and upstream registries. It forwards normal registry API traffic and caches blob downloads by digest.
Registry blobs are immutable and digest-addressed, which makes them ideal cache material. The goal here is simple:
- First pull: stream from upstream to client and cache at the same time.
- Next pull of the same digest: serve locally.
- Never store partial or corrupt blobs.
Implemented now:
- Proxying OCI API paths (notably blob GETs).
- Upstream auth challenge handling (Bearer token flow).
- CAS-style blob store on disk.
- Digest verification before accepting a cached blob.
- Atomic file writes.
Still evolving:
- Manifest caching strategy.
- Better cache-hit observability and metrics.
- Concurrency storm handling for the same digest.
Requirements:
- Go 1.25+
- podman and/or regctl for manual testing
Run the proxy:
make runBy default it listens on :8080 and stores cache under /tmp/cache/oci.
Configure local client tools:
regctl registry set --tls disabled localhost:8080 -v traceExample pull through the proxy:
podman image pull localhost:8080/docker.io/alpine:latest --tls-verify=falseHigh-level flow:
Client -> Proxy Handler -> Upstream Registry \-> CAS (local disk cache)
Blob GET flow:
- Parse request and classify it as blob GET.
- Check local CAS for digest.
- On hit: stream cached file to client.
- On miss: fetch from upstream, stream to client, tee into CAS writer.
- Commit cache entry only if digest matches expected value.
Blob files are stored by digest path:
/tmp/cache/oci/blobs/<algorithm>/<prefix>/<hex>
Example:
/tmp/cache/oci/blobs/sha256/11/11c182...
Metadata is written separately by the metadata store.
The cache should keep these guarantees:
- Content-addressed: key is the OCI digest.
- Atomic writes: temp file + fsync + rename.
- No partial writes: failed streams are discarded.
- Digest validated before commit.
Key paths:
main.go: wiring for config, transport chain, stores, and router.internal/proxy/handler.go: request classification and blob cache logic.internal/cache/cas.go: blob writer lifecycle and metadata commit.internal/blobs/blobs.go: filesystem blob store and digest verification.internal/transport/auth.go: token challenge flow and retry.internal/transport/logger.go: verbose request/response tracing.pkg/stream/duplicator.go: stream duplication helpers.
- Clean cache directory.
- Start proxy with visible logs.
- Pull a medium image once (cold path).
- Show cached blob files created under
/tmp/cache/oci/blobs/sha256/.... - Pull the same image again (warm path).
- Show fewer upstream fetch logs / faster completion.
Example command sequence:
rm -rf /tmp/cache/oci
mkdir -p /tmp/cache/oci
make runIn another terminal:
regctl registry set --tls disabled localhost:8080 -v trace
podman image rm localhost:8080/docker.io/alpine:latest || true
podman image pull localhost:8080/docker.io/alpine:latest --tls-verify=false
find /tmp/cache/oci/blobs -type f | head
podman image rm localhost:8080/docker.io/alpine:latest || true
podman image pull localhost:8080/docker.io/alpine:latest --tls-verify=false