diff --git a/main.tf b/main.tf index 92402458..c45e8cd9 100644 --- a/main.tf +++ b/main.tf @@ -276,6 +276,12 @@ module "docker-registry-template" { templatefile("${path.root}/modules/docker-registry/nginx_registry.conf", {}) ) ), + "docker run -d --restart always --net host --name registry-ui -e NGINX_LISTEN_PORT=8080 -e NGINX_PROXY_PASS_URL=http://127.0.0.1:5000 -e DELETE_IMAGES=true -e SINGLE_REGISTRY=true -e SHOW_CONTENT_DIGEST=true -e SHOW_CATALOG_NB_TAGS=true -e CATALOG_ELEMENTS_LIMIT=1000 -e TAGLIST_PAGE_SIZE=100 -e REGISTRY_TITLE=viktorbarzin.me joxit/docker-registry-ui:latest", + # Deploy tag cleanup script (keep last 10 tags per image) and schedule daily at 2am before weekly GC + format("echo %s | base64 -d > /etc/docker-registry/cleanup-tags.sh && chmod +x /etc/docker-registry/cleanup-tags.sh", + base64encode(file("${path.root}/modules/docker-registry/cleanup-tags.sh")) + ), + "( crontab -l 2>/dev/null; echo '0 2 * * * /etc/docker-registry/cleanup-tags.sh 10 >> /var/log/registry-cleanup.log 2>&1' ) | crontab -", ] } @@ -298,7 +304,8 @@ module "docker-registry-vm" { # ports: # 5000 -> registry # 5001 -> metrics - # 5002 -> ngin proxy <-- use this to prevent races on the same blobs + # 5002 -> nginx proxy <-- use this to prevent races on the same blobs + # 8080 -> registry-ui (joxit/docker-registry-ui) } # module that provisions the proxmox host? diff --git a/modules/docker-registry/cleanup-tags.sh b/modules/docker-registry/cleanup-tags.sh new file mode 100644 index 00000000..fe51d51a --- /dev/null +++ b/modules/docker-registry/cleanup-tags.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Keeps only the N most recent tags per image in a Docker registry. +Uses filesystem modification times on the tag directories for speed. +Run garbage-collect after this to reclaim disk space.""" + +import json +import os +import sys +import urllib.request + +sys.stdout.reconfigure(line_buffering=True) + +REGISTRY = "http://127.0.0.1:5000" +KEEP = int(sys.argv[1]) if len(sys.argv) > 1 else 10 + +# Registry storage path (docker volume) +STORAGE = "/var/lib/docker/volumes/57b3f1c5fcc7f39c040e17072e10b4536245357d09340206683c04096d30b942/_data/docker/registry/v2/repositories" + +def api(path, method="GET", headers=None): + req = urllib.request.Request(f"{REGISTRY}{path}", method=method, headers=headers or {}) + try: + with urllib.request.urlopen(req, timeout=30) as r: + if method == "HEAD": + return dict(r.headers) + return json.loads(r.read()) + except Exception: + return None + +# Get all repos +catalog = api("/v2/_catalog") +if not catalog: + print("Failed to fetch catalog") + sys.exit(1) + +total_deleted = 0 + +for repo in catalog.get("repositories", []): + tags_dir = os.path.join(STORAGE, repo, "_manifests", "tags") + if not os.path.isdir(tags_dir): + continue + + # Get tags with their modification times from filesystem + tag_times = [] + for tag in os.listdir(tags_dir): + tag_path = os.path.join(tags_dir, tag) + if os.path.isdir(tag_path): + mtime = os.path.getmtime(tag_path) + tag_times.append((mtime, tag)) + + if len(tag_times) <= KEEP: + continue + + # Sort by mtime descending (newest first), delete everything past KEEP + tag_times.sort(reverse=True) + to_delete = tag_times[KEEP:] + + print(f"[{repo}] has {len(tag_times)} tags, deleting {len(to_delete)}, keeping {KEEP}") + + for _, tag in to_delete: + headers_resp = api(f"/v2/{repo}/manifests/{tag}", method="HEAD", headers={ + "Accept": "application/vnd.docker.distribution.manifest.v2+json" + }) + if not headers_resp: + continue + digest = headers_resp.get("Docker-Content-Digest") or headers_resp.get("docker-content-digest") + if digest: + result = api(f"/v2/{repo}/manifests/{digest}", method="DELETE") + total_deleted += 1 + + print(f" deleted {len(to_delete)} tags") + +print(f"\nDone. Deleted {total_deleted} total tags. Run garbage-collect to reclaim disk space.") diff --git a/modules/kubernetes/reverse_proxy/main.tf b/modules/kubernetes/reverse_proxy/main.tf index c34cf58c..3a72423a 100644 --- a/modules/kubernetes/reverse_proxy/main.tf +++ b/modules/kubernetes/reverse_proxy/main.tf @@ -151,6 +151,20 @@ module "proxmox" { rybbit_site_id = "190a7ad3e1c7" } +# https://registry.viktorbarzin.me/ +module "docker-registry-ui" { + source = "./factory" + name = "registry" + external_name = "docker-registry.viktorbarzin.lan" + port = 8080 + tls_secret_name = var.tls_secret_name + depends_on = [kubernetes_namespace.reverse-proxy] + extra_annotations = { + # Override middleware chain to remove rate-limit; the UI fires many API calls to list repos/tags + "traefik.ingress.kubernetes.io/router.middlewares" = "traefik-csp-headers@kubernetescrd,traefik-crowdsec@kubernetescrd,traefik-authentik-forward-auth@kubernetescrd" + } +} + # https://valchedrym.viktorbarzin.me/ module "valchedrym" { source = "./factory"