Zot is an open-source container registry built entirely on the OCI Distribution Specification, with no Docker Registry v2 compatibility layer and no non-OCI baggage. It's the only registry that's both OCI-native and CNCF-conformant, making it popular for air-gapped environments, GitOps pipelines, and supply-chain-secure container deployments. Zot supports pluggable storage backends (local disk, S3), image replication to peer registries, and online garbage collection — but when any of these subsystems stalls, the failure surface is broad: push operations return 500 errors, pulls serve stale layers, replication drifts silently, and garbage collection eats disk I/O without finishing. Vigilmon gives you external monitoring for Zot API health, push/pull throughput, storage backend health, replication sync status, and garbage collection metrics so you catch registry failures before they block CI/CD pipelines.
What You'll Set Up
- Zot API health and readiness via HTTP monitors
- Push and pull throughput tracking
- Storage backend health monitoring
- Replication sync status checks
- Garbage collection activity monitoring
Prerequisites
- Zot v2.0+ running (via binary, Docker, or Kubernetes)
- Zot API accessible at your registry URL (default port 5000)
zliCLI ororasCLI available for push/pull tests- A free Vigilmon account
Step 1: Monitor Zot API Health and Readiness
Zot exposes a health endpoint at /_zot/metrics (Prometheus format) and a standard OCI Distribution Spec health check at /v2/. The /v2/ endpoint returns 200 OK with an empty JSON object when the registry is ready to serve requests. Use an HTTP monitor for immediate alerting on registry downtime:
- In Vigilmon, click Add Monitor → HTTP Monitor.
- Set the URL to
https://your-zot-registry.example.com/v2/. - Set Expected HTTP status to
200. - Set Check interval to
1 minute. - Add an Authorization header if your registry requires authentication:
Basic <base64(user:password)>.
For a more thorough health check via cron heartbeat:
#!/bin/bash
# /usr/local/bin/zot-health-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/abc123"
ZOT_URL="${ZOT_URL:-http://localhost:5000}"
ZOT_AUTH="${ZOT_AUTH:-}" # Set to "user:password" if auth required
AUTH_ARGS=""
if [ -n "$ZOT_AUTH" ]; then
AUTH_ARGS="-u $ZOT_AUTH"
fi
# Check the OCI distribution spec v2 endpoint
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $AUTH_ARGS \
"${ZOT_URL}/v2/")
if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "401" ]; then
# 401 means auth is required but the registry is up
echo "Zot API healthy: HTTP $HTTP_STATUS"
curl -s "$HEARTBEAT_URL"
else
echo "Zot API unhealthy: HTTP $HTTP_STATUS"
exit 1
fi
Install and enable:
chmod +x /usr/local/bin/zot-health-check.sh
(crontab -l 2>/dev/null; echo "*/1 * * * * /usr/local/bin/zot-health-check.sh >> /var/log/zot-health.log 2>&1") | crontab -
The HTTP monitor catches the most common failure modes: Zot process crash, port binding failure, and TLS certificate expiry. The cron heartbeat adds authentication and response body validation.
Step 2: Track Push and Pull Throughput
Push and pull are Zot's primary workload. Slow pushes block CI artifact publishing; slow pulls delay container startup in your deployment pipeline. Monitor the full push/pull round-trip with a test image:
#!/bin/bash
# /usr/local/bin/zot-push-pull-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/def456"
ZOT_URL="${ZOT_URL:-http://localhost:5000}"
ZOT_USER="${ZOT_USER:-admin}"
ZOT_PASS="${ZOT_PASS:-admin}"
TEST_REPO="vigilmon/healthcheck"
TEST_TAG="probe-$(date +%s)"
PUSH_THRESHOLD=15 # seconds
PULL_THRESHOLD=10 # seconds
# Requires 'oras' CLI: https://oras.land/
if ! command -v oras >/dev/null 2>&1; then
echo "oras CLI not found — install from https://oras.land/"
exit 1
fi
# Create a small test artifact
TEST_FILE="/tmp/zot-probe-$$"
echo "vigilmon-probe-$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TEST_FILE"
# Login to registry
echo "$ZOT_PASS" | oras login --username "$ZOT_USER" --password-stdin \
"${ZOT_URL#*//}" >/dev/null 2>&1
# Measure push latency
START=$(date +%s)
if oras push --no-tls "${ZOT_URL#*//}/${TEST_REPO}:${TEST_TAG}" \
"$TEST_FILE:text/plain" >/dev/null 2>&1; then
END=$(date +%s)
PUSH_ELAPSED=$(( END - START ))
else
echo "Push failed for ${TEST_REPO}:${TEST_TAG}"
rm -f "$TEST_FILE"
exit 1
fi
# Measure pull latency
PULL_DIR="/tmp/zot-pull-$$"
mkdir -p "$PULL_DIR"
START=$(date +%s)
if oras pull --no-tls --output "$PULL_DIR" \
"${ZOT_URL#*//}/${TEST_REPO}:${TEST_TAG}" >/dev/null 2>&1; then
END=$(date +%s)
PULL_ELAPSED=$(( END - START ))
else
echo "Pull failed for ${TEST_REPO}:${TEST_TAG}"
rm -rf "$TEST_FILE" "$PULL_DIR"
exit 1
fi
echo "Push: ${PUSH_ELAPSED}s, Pull: ${PULL_ELAPSED}s"
rm -f "$TEST_FILE"
rm -rf "$PULL_DIR"
if [ "$PUSH_ELAPSED" -lt "$PUSH_THRESHOLD" ] && \
[ "$PULL_ELAPSED" -lt "$PULL_THRESHOLD" ]; then
curl -s "$HEARTBEAT_URL"
else
echo "Throughput degraded: push=${PUSH_ELAPSED}s pull=${PULL_ELAPSED}s"
exit 1
fi
Schedule this every 10 minutes with a matching Vigilmon heartbeat interval. Slow pushes above 15 seconds for a tiny artifact indicate storage backend I/O saturation or blob deduplication taking longer than expected on a large layer store. Slow pulls often indicate the manifest and layer cache is cold, triggering storage reads that compete with ongoing pushes.
Step 3: Monitor Storage Backend Health
Zot's storage backend is its most critical dependency. Whether using local disk or S3-compatible object storage, storage failures cause silent data corruption, failed uploads, and incomplete pulls. Monitor storage health separately from API health:
#!/bin/bash
# /usr/local/bin/zot-storage-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/ghi789"
ZOT_STORAGE_ROOT="${ZOT_STORAGE:-/var/lib/registry}"
DISK_THRESHOLD=80 # percent — alert before Zot hits its own configurable limit
INODE_THRESHOLD=85 # percent
# Check disk usage at the storage root
DISK_USED=$(df "$ZOT_STORAGE_ROOT" 2>/dev/null | awk 'NR==2 {gsub(/%/, ""); print $5}')
if [ -z "$DISK_USED" ]; then
echo "Storage root not accessible: $ZOT_STORAGE_ROOT"
exit 1
fi
if [ "$DISK_USED" -ge "$DISK_THRESHOLD" ]; then
echo "Storage critical: ${DISK_USED}% used at $ZOT_STORAGE_ROOT"
exit 1
fi
# Check inode exhaustion — many small OCI layer blobs can exhaust inodes
# before disk bytes are full
INODE_USED=$(df -i "$ZOT_STORAGE_ROOT" 2>/dev/null | awk 'NR==2 {gsub(/%/, ""); print $5}')
if [ -n "$INODE_USED" ] && [ "$INODE_USED" -ge "$INODE_THRESHOLD" ]; then
echo "Inode exhaustion: ${INODE_USED}% used at $ZOT_STORAGE_ROOT"
exit 1
fi
# Verify write access to the storage root
TEST_FILE="$ZOT_STORAGE_ROOT/.vigilmon-write-test-$$"
if touch "$TEST_FILE" 2>/dev/null; then
rm -f "$TEST_FILE"
else
echo "Storage write test failed — filesystem may be read-only"
exit 1
fi
# Count blobs to verify the repository structure is intact
BLOB_DIR="$ZOT_STORAGE_ROOT"
if [ -d "$BLOB_DIR" ]; then
REPO_COUNT=$(find "$BLOB_DIR" -maxdepth 1 -type d 2>/dev/null | tail -n +2 | wc -l)
echo "Storage OK: ${DISK_USED}% disk, ${INODE_USED:-N/A}% inodes, ${REPO_COUNT} repos"
fi
curl -s "$HEARTBEAT_URL"
Set the Vigilmon heartbeat to 10 minutes. Inode exhaustion is a particularly common failure mode for Zot because each OCI layer blob is a separate file — a registry with 500 images and 10 layers each creates 5,000+ files before manifests and index files. Monitor inodes separately from bytes.
If you use S3 as a backend, add an S3 reachability check:
# Check S3 backend connectivity (requires aws CLI)
aws s3 ls "s3://${ZOT_S3_BUCKET}/" --region "${ZOT_S3_REGION}" \
>/dev/null 2>&1 && echo "S3 backend OK" || echo "S3 backend unreachable"
Step 4: Monitor Replication Sync Status
Zot supports OCI image replication to peer registries via its sync extension. Replication lag means your disaster recovery registry is serving stale images, and geo-distributed pulls are serving stale layers. Monitor replication health via Zot's metrics endpoint:
#!/bin/bash
# /usr/local/bin/zot-replication-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/jkl012"
ZOT_URL="${ZOT_URL:-http://localhost:5000}"
ZOT_AUTH="${ZOT_AUTH:-}"
MAX_SYNC_LAG_MINUTES=30 # Alert if last sync was more than 30 minutes ago
AUTH_ARGS=""
if [ -n "$ZOT_AUTH" ]; then
AUTH_ARGS="-u $ZOT_AUTH"
fi
# Check Zot metrics for sync error counters
METRICS=$(curl -s $AUTH_ARGS "${ZOT_URL}/_zot/metrics" 2>/dev/null)
if [ -z "$METRICS" ]; then
echo "Metrics endpoint unreachable — is _zot.extensions.metrics enabled?"
# Don't fail the heartbeat if metrics aren't configured
curl -s "$HEARTBEAT_URL"
exit 0
fi
# Check for sync error counts
SYNC_ERRORS=$(echo "$METRICS" | grep "^zot_sync_errors_total" | \
awk '{sum+=$2} END {print sum+0}')
if [ "${SYNC_ERRORS:-0}" -gt 0 ]; then
echo "Replication errors detected: sync_errors_total=$SYNC_ERRORS"
exit 1
fi
# Check for sync repo errors specifically
REPO_ERRORS=$(echo "$METRICS" | grep "sync_repo" | grep -v "^#" | \
awk '{sum+=$2} END {print sum+0}')
echo "Replication status: errors=$SYNC_ERRORS repo_errors=${REPO_ERRORS:-0}"
curl -s "$HEARTBEAT_URL"
Also configure a Vigilmon HTTP monitor directly against your replica registry:
- Add an HTTP monitor for
https://your-replica-registry.example.com/v2/. - Set Expected HTTP status to
200. - Set Check interval to
2 minutes.
If the replica goes down while the primary is up, you'll get separate alerts for each — giving you clear signal about whether the failure is primary vs. replica.
Step 5: Monitor Garbage Collection Activity
Zot supports online garbage collection that runs in the background to reclaim storage from deleted manifests and unreferenced blobs. GC that runs continuously and never finishes (due to a large blob store or slow storage) causes I/O saturation that degrades push/pull performance. GC that never runs causes unbounded disk growth. Monitor GC health via metrics:
#!/bin/bash
# /usr/local/bin/zot-gc-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/mno345"
ZOT_URL="${ZOT_URL:-http://localhost:5000}"
ZOT_STORAGE="${ZOT_STORAGE:-/var/lib/registry}"
ZOT_AUTH="${ZOT_AUTH:-}"
GC_LOG="/var/log/zot-gc.log"
AUTH_ARGS=""
if [ -n "$ZOT_AUTH" ]; then
AUTH_ARGS="-u $ZOT_AUTH"
fi
# Query metrics for GC-related counters
METRICS=$(curl -s $AUTH_ARGS "${ZOT_URL}/_zot/metrics" 2>/dev/null)
if [ -n "$METRICS" ]; then
# Extract GC run count and duration metrics
GC_RUNS=$(echo "$METRICS" | grep "^zot_gc_runs_total" | \
awk '{print $2+0}' | head -1)
GC_ERRORS=$(echo "$METRICS" | grep "^zot_gc_errors_total" | \
awk '{print $2+0}' | head -1)
GC_BLOBS=$(echo "$METRICS" | grep "^zot_gc_blobs_deleted_total" | \
awk '{print $2+0}' | head -1)
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) gc_runs=${GC_RUNS:-0} gc_errors=${GC_ERRORS:-0} blobs_deleted=${GC_BLOBS:-0}" >> "$GC_LOG"
if [ "${GC_ERRORS:-0}" -gt 0 ]; then
echo "GC errors detected: ${GC_ERRORS} errors in ${GC_RUNS:-0} runs"
exit 1
fi
echo "GC OK: runs=${GC_RUNS:-0}, blobs_deleted=${GC_BLOBS:-0}"
fi
# Independent storage size check to detect GC stagnation
STORAGE_MB=$(du -sm "$ZOT_STORAGE" 2>/dev/null | awk '{print $1}')
if [ -n "$STORAGE_MB" ]; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) storage_mb=$STORAGE_MB" >> "$GC_LOG"
fi
curl -s "$HEARTBEAT_URL"
Set the Vigilmon heartbeat to 30 minutes. Review the GC log weekly — a storage size that only grows and never shrinks after deletions means GC isn't running or is failing silently. A storage size that fluctuates with deletions and GC runs means GC is healthy.
To trigger a manual GC run via the Zot API when needed:
# Trigger GC via the Zot management API (requires admin credentials)
curl -s -X POST "${ZOT_URL}/_zot/forcegc" \
-H "Authorization: Bearer YOUR_TOKEN"
Step 6: Set Up Alert Channels
Configure Vigilmon to route Zot registry alerts appropriately:
- Go to Alert Channels in Vigilmon.
- Add a Slack webhook to
#platform-alertsor#registry-ops. - Add PagerDuty for the API health and push/pull monitors — registry downtime blocks all CI/CD pipelines immediately.
- Set Consecutive failures before alert to
1for the API health monitor — there's no valid reason for the registry API to be down briefly. - Set Consecutive failures before alert to
2for throughput and GC monitors — transient I/O spikes are normal.
Add maintenance windows for Zot upgrades and GC-intensive operations:
curl -X POST https://vigilmon.online/api/maintenance \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"monitor_id": "MONITOR_ID",
"duration_minutes": 20,
"reason": "zot registry upgrade and GC run"
}'
Summary
| Monitor | Target | What It Catches |
|---|---|---|
| HTTP monitor | /v2/ endpoint | Registry crash, port binding failure, TLS expiry |
| Cron heartbeat (push/pull) | Timed ORAS push + pull cycle | Throughput degradation, storage backend I/O saturation |
| Cron heartbeat (storage) | Disk + inode usage + write test | Disk full, inode exhaustion, read-only filesystem |
| Cron heartbeat (replication) | Metrics sync error counters + replica HTTP | Replication lag, sync errors, replica downtime |
| Cron heartbeat (GC) | GC metrics + storage size trend | GC stagnation, GC errors, unbounded disk growth |
Zot's OCI-native design makes it lean and auditable, but its storage backend, replication engine, and garbage collector each have failure modes that don't surface as API errors until they've already degraded your pipeline. Vigilmon's external checks give you visibility into the full operational surface — so you know about registry health problems before your CI/CD pipeline starts failing pushes.
Start monitoring for free at vigilmon.online.