OCI Distribution — maintained by the CNCF as the reference implementation of the OCI Distribution Specification — is the open-source container registry that powers Docker Hub, GitHub Container Registry, Amazon ECR Public, and many self-hosted deployments. It implements the full OCI Distribution API: image push/pull, manifest storage, blob uploads, content-addressable layer storage, and garbage collection. Teams run self-hosted OCI Distribution instances as internal artifact stores, CI/CD artifact staging areas, and air-gapped registry mirrors. When OCI Distribution goes down — the registry process crashes, the storage backend becomes unavailable, or the filesystem fills up — every image push from CI and every pod start from Kubernetes fails simultaneously. Vigilmon gives you external monitoring that watches your OCI Distribution instance from outside the process and alerts before registry outages cascade into build and deployment failures.
What You'll Build
- Vigilmon HTTP monitors for the OCI Distribution API endpoints
- A push/pull validation heartbeat that verifies end-to-end registry operations work
- A storage backend health probe
- A garbage collection job monitor
- Alert channels routed to your platform team
Prerequisites
- A running OCI Distribution instance (v2 registry, typically
distribution/distributionDocker image) - The registry exposed over HTTPS (required for Docker and containerd clients in production)
- At least one storage backend configured (filesystem, S3, Azure Blob, or GCS)
- A free account at vigilmon.online
Why Monitoring OCI Distribution Matters
OCI Distribution is a synchronous critical path dependency for your entire container infrastructure. Every component that starts, builds, or deploys containers depends on it:
Registry process unavailability is the most visible failure mode. When the registry process crashes or the API becomes unreachable, every Kubernetes pod using imagePullPolicy: Always fails to start, every CI job that pushes artifacts fails at the push step, and any developer docker pull returns a connection error. The blast radius is the entire container workload.
Storage backend failure is more dangerous than process failure because it may not immediately surface as a registry error. If the registry uses an S3 backend and the S3 bucket becomes inaccessible, the registry API may continue responding to manifest HEAD requests (served from cache) while all blob fetches return 500. Containers appear to be pulling but receive incomplete layers.
Filesystem exhaustion on filesystem-backend registries causes all writes to fail with 500 errors while reads continue to succeed. Push operations from CI fail; the registry appears healthy to read-only monitors.
Garbage collection (GC) lag causes the registry storage to grow unbounded. Unreferenced layers accumulate over time from CI builds that push new images but never clean up old ones. Eventually storage exhaustion triggers a write failure cascade.
Token authentication service failures affect registries using registry:2 token auth. The registry API returns 401 with a Www-Authenticate header pointing to the token service; if the token service is down, no client can authenticate and all operations fail.
External monitoring with Vigilmon catches these from the perspective of clients that depend on the registry.
Step 1: Monitor the OCI Distribution API Endpoint
OCI Distribution exposes a /v2/ base endpoint that all compliant clients probe first. This is the universal registry health signal:
# For anonymous/public registries — expect 200
curl -I https://registry.yourdomain.com/v2/
# Expected: HTTP/2 200
# For authenticated registries — expect 401 with Www-Authenticate header
curl -I https://registry.yourdomain.com/v2/
# Expected: HTTP/2 401
# Www-Authenticate: Bearer realm="https://registry.yourdomain.com/auth/token",service="registry.yourdomain.com"
Both 200 and 401 confirm the registry process is alive. A 503, connection refused, or timeout means the registry is down.
In the Vigilmon dashboard:
- Go to Add Monitor → HTTP
- Configure:
| Field | Value |
|---|---|
| Name | OCI Distribution /v2/ |
| URL | https://registry.yourdomain.com/v2/ |
| Method | GET |
| Expected status | 200, 401 |
| Check interval | 30 seconds |
| Timeout | 10 seconds |
| Alert after | 2 consecutive failures |
Use 30-second intervals for the /v2/ check — registry outages have an immediate and large blast radius, so faster detection pays off.
Step 2: Monitor the Registry Debug/Health Endpoint
OCI Distribution ships with a debug server that exposes a health endpoint. Enable it in your registry configuration:
# config.yml snippet
http:
addr: :5000
debug:
addr: :5001
prometheus:
enabled: true
path: /metrics
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
Then monitor the health endpoint:
curl http://registry.yourdomain.com:5001/debug/health
# Expected: {} (empty JSON = healthy)
In Vigilmon:
| Field | Value |
|---|---|
| Name | OCI Distribution /debug/health |
| URL | http://registry.yourdomain.com:5001/debug/health |
| Method | GET |
| Expected status | 200 |
| Response body contains | {} |
| Check interval | 1 minute |
| Timeout | 10 seconds |
| Alert after | 2 consecutive failures |
The health endpoint checks storage backend connectivity — if storage is unhealthy, the health endpoint returns a non-empty JSON body with the error details.
Step 3: Monitor Prometheus Metrics Endpoint
If you've enabled Prometheus metrics (common in production deployments), add a metrics monitor:
curl http://registry.yourdomain.com:5001/metrics | head -20
# Expected: Prometheus text format with registry_* metrics
In Vigilmon:
| Field | Value |
|---|---|
| Name | OCI Distribution /metrics |
| URL | http://registry.yourdomain.com:5001/metrics |
| Method | GET |
| Expected status | 200 |
| Response body contains | registry_ |
| Check interval | 1 minute |
| Timeout | 10 seconds |
| Alert after | 2 consecutive failures |
Step 4: End-to-End Push/Pull Validation Heartbeat
The /v2/ check confirms the API is alive but doesn't validate that push and pull actually work end-to-end. Add a CronJob that performs a real push and pull cycle:
# oci-distribution-e2e-probe.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: oci-distribution-e2e-probe
namespace: monitoring
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: e2e-probe
image: alpine:latest
env:
- name: REGISTRY
value: "registry.yourdomain.com"
- name: HEARTBEAT_URL
valueFrom:
secretKeyRef:
name: vigilmon-secrets
key: oci-e2e-heartbeat-url
- name: REGISTRY_USER
valueFrom:
secretKeyRef:
name: registry-credentials
key: username
- name: REGISTRY_PASS
valueFrom:
secretKeyRef:
name: registry-credentials
key: password
command:
- /bin/sh
- -c
- |
apk add --quiet curl skopeo
PROBE_TAG="monitoring/probe:$(date +%s)"
# Push a minimal image manifest directly via the OCI Distribution API
# Get auth token first
TOKEN=$(curl -sf -u "$REGISTRY_USER:$REGISTRY_PASS" \
"https://$REGISTRY/auth/token?service=$REGISTRY&scope=repository:monitoring/probe:push,pull" \
| grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "ERROR: Failed to obtain registry token" >&2
exit 1
fi
# Push an empty blob (minimal test payload)
EMPTY_BLOB='sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
UPLOAD_URL=$(curl -sf -D - \
-H "Authorization: Bearer $TOKEN" \
-X POST \
"https://$REGISTRY/v2/monitoring/probe/blobs/uploads/" \
| grep -i "^location:" | tr -d '\r' | cut -d' ' -f2)
if [ -z "$UPLOAD_URL" ]; then
echo "ERROR: Failed to initiate blob upload" >&2
exit 1
fi
# Check the tag list endpoint to confirm pull access works
TAG_RESPONSE=$(curl -sf \
-H "Authorization: Bearer $TOKEN" \
"https://$REGISTRY/v2/monitoring/probe/tags/list" || echo "empty")
echo "E2E registry probe completed: push initiation OK, tag list accessible"
curl -fsS --max-time 10 "$HEARTBEAT_URL" > /dev/null
echo "E2E heartbeat sent"
For a simpler approach using skopeo:
#!/bin/bash
# oci-distribution-skopeo-probe.sh
set -e
REGISTRY="registry.yourdomain.com"
HEARTBEAT_URL="${VIGILMON_OCI_E2E_HEARTBEAT_URL}"
# Copy a minimal public image into the registry as a probe
if skopeo copy \
--dest-creds "$REGISTRY_USER:$REGISTRY_PASS" \
docker://alpine:latest \
"docker://$REGISTRY/monitoring/probe:latest" 2>&1; then
echo "Push probe succeeded"
# Verify pull works
if skopeo inspect \
--creds "$REGISTRY_USER:$REGISTRY_PASS" \
"docker://$REGISTRY/monitoring/probe:latest" > /dev/null 2>&1; then
curl -fsS --max-time 10 "$HEARTBEAT_URL" > /dev/null
echo "Pull probe succeeded, heartbeat sent"
else
echo "ERROR: Pull probe failed after successful push" >&2
exit 1
fi
else
echo "ERROR: Push probe failed" >&2
exit 1
fi
Create the heartbeat monitor in Vigilmon:
- Add Monitor → Heartbeat
- Name:
OCI Distribution end-to-end push/pull - Expected interval: 5 minutes
- Grace period: 5 minutes
Step 5: Monitor Garbage Collection Jobs
OCI Distribution GC must run periodically to reclaim storage from deleted images. Monitor that GC is completing on schedule:
#!/bin/bash
# registry-gc-probe.sh
set -e
HEARTBEAT_URL="${VIGILMON_OCI_GC_HEARTBEAT_URL}"
REGISTRY_CONFIG="/etc/docker/registry/config.yml"
STORAGE_DIR="/var/lib/registry"
# Run garbage collection
registry garbage-collect "$REGISTRY_CONFIG" --delete-untagged 2>&1
# Check that the storage directory is accessible and not full
USAGE=$(df "$STORAGE_DIR" | awk 'NR==2 {print $5}' | tr -d '%')
THRESHOLD=90
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "WARN: Registry storage at ${USAGE}% (threshold: ${THRESHOLD}%) after GC" >&2
exit 1
fi
echo "GC complete, storage at ${USAGE}%"
curl -fsS --max-time 10 "$HEARTBEAT_URL" > /dev/null
echo "GC heartbeat sent"
Deploy as a Kubernetes CronJob that runs daily (adjust to your GC schedule):
# registry-gc-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: registry-gc
namespace: registry
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: gc
image: registry:2
command:
- /bin/sh
- -c
- |
/entrypoint.sh garbage-collect /etc/docker/registry/config.yml --delete-untagged
wget -qO- "$VIGILMON_GC_HEARTBEAT_URL"
env:
- name: VIGILMON_GC_HEARTBEAT_URL
valueFrom:
secretKeyRef:
name: vigilmon-secrets
key: oci-gc-heartbeat-url
volumeMounts:
- name: registry-storage
mountPath: /var/lib/registry
- name: registry-config
mountPath: /etc/docker/registry
volumes:
- name: registry-storage
persistentVolumeClaim:
claimName: registry-pvc
- name: registry-config
configMap:
name: registry-config
Create the heartbeat monitor in Vigilmon:
- Add Monitor → Heartbeat
- Name:
OCI Distribution garbage collection - Expected interval: 24 hours
- Grace period: 2 hours
Step 6: Configure Alert Channels
Route OCI Distribution alerts by severity:
For the /v2/ API monitor (highest severity — entire container infrastructure blocked):
- In Vigilmon: Alert Channels → Add Channel → PagerDuty
- Add your infrastructure on-call rotation
- Assign to
OCI Distribution /v2/— this is a P1 incident
For storage and GC monitors (important but not immediate):
- Alert Channels → Add Channel → Slack Webhook
- Add your
#infrastructureor#platform-engineeringwebhook - Assign to health, metrics, GC, and E2E heartbeat monitors
For the end-to-end push/pull heartbeat (CI impact):
- Alert Channels → Add Channel → Slack Webhook
- Add your
#ci-cd-alertswebhook - Assign to the
OCI Distribution end-to-end push/pullheartbeat
Create the Kubernetes secrets:
kubectl create secret generic vigilmon-secrets \
--from-literal=oci-e2e-heartbeat-url='https://vigilmon.online/heartbeat/YOUR_E2E_TOKEN' \
--from-literal=oci-gc-heartbeat-url='https://vigilmon.online/heartbeat/YOUR_GC_TOKEN' \
-n monitoring
What You're Now Monitoring
| Component | Monitor Type | What It Detects |
|---|---|---|
| /v2/ API | HTTP GET (30s) | Registry process down — P1 for all container workloads |
| /debug/health | HTTP GET (1 min) | Storage backend failure, internal health degraded |
| /metrics | HTTP GET (1 min) | Metrics server and internal instrumentation unavailable |
| End-to-end push/pull | Heartbeat (5 min) | Push or pull broken at application layer |
| Garbage collection | Heartbeat (24 hr) | GC not running, storage growing unbounded |
Conclusion
OCI Distribution is the foundation of your container artifact supply chain — when it fails, everything fails simultaneously. The /v2/ endpoint is the single most important signal (it reflects the registry process status immediately) but it misses the subtler failures: storage backend unavailability, filesystem exhaustion, and GC lag. The combination of API probes, end-to-end push/pull validation, and GC heartbeats gives you complete visibility into the registry's health as a system, not just as a process.
Get started free at vigilmon.online. The /v2/ HTTP probe and end-to-end push/pull heartbeat together give you the two most critical OCI Distribution signals in under ten minutes: is the API alive, and can clients actually push and pull images?