Docker Registry Incident: Fixing a Broken Private Registry in Production
The Incident
CI pipelines start failing at 09:14 with: error parsing HTTP 400 response body: invalid character '<'. Engineers cannot push images. Pulls of existing images still work. Deployments are blocked.
---
Step 1: Check the Registry Directly
curl -I https://registry.internal/v2/
HTTP/1.1 400 Bad Request
Content-Type: text/html
HTML response to a Docker API call — a load balancer or nginx is intercepting the request and returning an error page before it reaches the registry.
---
Step 2: Check the Registry Container
docker logs registry --tail 50
time="2026-08-20T09:12:34Z" level=warning msg="repository does not exist" go.version=go1.21
time="2026-08-20T09:12:35Z" level=error msg="response completed with error" err="filesystem: mkdir /var/lib/registry/docker/registry/v2/repositories/myapp: no space left on device"
The registry container's volume is full. Writes fail. The load balancer's health check detects the registry returning 500s and starts serving its own error page instead.
---
Step 3: Find the Disk Usage
docker exec registry du -sh /var/lib/registry/docker/registry/v2/
48G /var/lib/registry/docker/registry/v2/
docker exec registry du -sh /var/lib/registry/docker/registry/v2/repositories/*/
12G /var/lib/registry/.../myapp
8G /var/lib/registry/.../frontend
18G /var/lib/registry/.../ml-model
The ML model image has accumulated 18 GB. The team has been pushing new versions daily without deleting old ones.
---
Step 4: Immediate Fix — Free Space
Identify old tags to delete:
curl -s https://registry.internal/v2/ml-model/tags/list | jq .
{"name":"ml-model","tags":["v1.0","v1.1","v1.2",...,"v8.4","v8.5","v8.6"]}
86 versions, only the last 3 are needed. Delete the rest via the registry API:
for tag in v1.0 v1.1 v1.2 ... v8.3; do
digest=$(curl -s -I -H "Accept: application/vnd.docker.distribution.manifest.v2+json" https://registry.internal/v2/ml-model/manifests/$tag | grep Docker-Content-Digest | tr -d '
' | awk '{print $2}')
curl -X DELETE https://registry.internal/v2/ml-model/manifests/$digest
done
Deleting manifests marks them for removal but does not free disk — you must run garbage collection:
docker exec registry registry garbage-collect /etc/docker/registry/config.yml
This freed 15 GB. The registry came back online within 2 minutes.
---
Step 5: The TLS Certificate Issue
During the investigation, the team checked the cert and found it expires in 6 days:
echo | openssl s_client -connect registry.internal:443 2>/dev/null | openssl x509 -noout -dates
notAfter=Aug 26 00:00:00 2026 GMT
Renewed the Let's Encrypt cert immediately (was handled by certbot on the host):
certbot renew --force-renewal -d registry.internal
nginx -s reload
---
Prevention
1. Automated garbage collection — add a scheduled job:
# /etc/cron.d/registry-gc
0 2 * * * root docker exec registry registry garbage-collect /etc/docker/registry/config.yml >> /var/log/registry-gc.log 2>&1
2. Retention policy in CI — the push pipeline now deletes images older than 30 days:
# In CI, after push
CUTOFF=$(date -d '30 days ago' +%s)
# Delete tags older than cutoff...
3. Disk alerting at 75% on the registry volume.
4. Certificate expiry monitoring — add the registry to the cert monitoring tool.
---
Post-Mortem Summary
Three independent issues converged: unbounded image accumulation (no retention policy), no disk alerting, and a certificate approaching expiry that would have caused a separate outage in 6 days.
Practice Docker registry work in the ShellGenius Docker Labs — the registry-push challenge covers pushing, tagging, and managing images in a private registry.