Compare commits

...

6 Commits

Author SHA1 Message Date
duffyduck 1a1b4500d6 Korrupte/verwaiste Repos sichtbar machen + Live-GC sicher
Zwei Probleme nach dem alten --delete-untagged-Schaden:

1) Repos mit Tags aber ohne _layers (verwaist) tauchen nicht im
   Registry-Katalog auf -> waren in der Web-Oberflaeche unsichtbar und
   nicht loeschbar. images() listet jetzt zusaetzlich direkt aus dem
   Dateisystem (scan_fs_repositories/repo_tags), Vereinigung mit dem
   Katalog. So sind auch korrupte Repos sichtbar und ueber den
   Loeschen-Button entfernbar.

2) Die GC laeuft als separater Prozess auf dem gemounteten Speicher.
   Der In-Memory-Blob-Cache der laufenden Registry hielt geloeschte
   Blobs faelschlich fuer vorhanden -> erneuter Pull legte keine
   _layers-Verknuepfung an (Image fehlte im Katalog). Cache via
   REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR="" deaktiviert -> Live-GC sicher.

Verifiziert: korruptes Repo -> in Liste sichtbar -> Loeschen -> GC ->
erneut pushen -> Katalog korrekt und docker pull erfolgreich, ohne
Registry-Neustart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:06:18 +02:00
duffyduck b24bd96917 GC: --delete-untagged entfernen (zerstoerte Multi-Arch-Images)
registry garbage-collect --delete-untagged loescht bei Multi-Arch-Images
die per-Architektur-Manifeste, die nur ueber die Manifest-Liste (Index)
referenziert sind. Die Liste/der Tag bleibt, die Kind-Manifeste sind weg
-> beim Pull "manifest unknown".

Reproduziert: zwei Multi-Arch-Images, garbage-collect --delete-untagged
markiert die Kind-Manifeste als "eligible for deletion" und loescht sie;
danach schlaegt "skopeo copy --all" mit manifest unknown fehl. Ohne das
Flag bleiben die Images intakt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:41:31 +02:00
duffyduck cd5beeedd3 Verwaiste Tags loeschbar machen
Ein Tag konnte im Katalog stehen, dessen Manifest aber nicht mehr
abrufbar war (Rest aus frueherem Loeschen/GC). Der Loeschen-Button
schlug dann mit "Tag nicht gefunden" fehl, der Eintrag liess sich nicht
entfernen.

delete_image entfernt jetzt zusaetzlich den Tag-Verweis direkt aus dem
Speicher: faellt der Manifest-Lookup ueber die API aus, wird der
verwaiste Tag trotzdem entfernt (und bei letztem Tag das Repository).
Pfad-Sicherheit in _safe_repo_path() gebuendelt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:15:58 +02:00
duffyduck cd9e33bdfe Auge zum Ein-/Ausblenden bei Kennwort und Access Token
Kennwort- und Access-Token-Feld haben jetzt ein Augen-Symbol, mit dem
die Eingabe sichtbar gemacht und kontrolliert werden kann. Umgesetzt
ueber ein wiederverwendbares pw_field-Macro (gilt fuer Einzel- und
Bulk-Formular).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:30:37 +02:00
duffyduck 4a11aabdf3 Leeres Repository nach Loeschen des letzten Tags entfernen
Die Registry-API loescht nur Manifeste/Tags, das Repository-Verzeichnis
bleibt im Speicher und damit im Katalog ("keine Tags") stehen. Da der
Auth-Container den Registry-Speicher gemountet hat, wird das leere
Verzeichnis jetzt entfernt:
- beim Loeschen des letzten Tags direkt
- bei der Garbage Collection werden zusaetzlich alle Tag-losen
  Repositories aufgeraeumt (fuer bereits vorhandene Reste)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:10:27 +02:00
duffyduck b273098b50 Compose: Variablen mit Default aufloesen (${VAR:-default})
Viele docker-compose-Dateien (z. B. Graylog) setzen Images per
${IMAGE:-default}. Bisher wurde alles mit "$" uebersprungen, sodass genau
diese Images nicht gefunden wurden. Jetzt werden ${VAR:-default} und
${VAR-default} mit ihrem Standardwert aufgeloest; nur Variablen ohne
Default (${VAR}, ${VAR:?...}) bleiben uebersprungen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:59:01 +02:00
5 changed files with 201 additions and 29 deletions
+4 -3
View File
@@ -113,9 +113,10 @@ uebersprungen &ndash; so wird ein Doppel-Upload vermieden, auch wenn der Tag (z.
Im selben Tab kann der Inhalt einer `docker-compose.yml` in ein Textfeld eingefuegt Im selben Tab kann der Inhalt einer `docker-compose.yml` in ein Textfeld eingefuegt
oder eine Datei hochgeladen werden. Mit **Alle Images holen** werden alle oder eine Datei hochgeladen werden. Mit **Alle Images holen** werden alle
`image:`-Eintraege der Services in die Registry kopiert. Eintraege mit nicht `image:`-Eintraege der Services in die Registry kopiert. Variablen mit Standardwert
aufgeloesten Variablen (z. B. `${TAG}`) oder die bereits auf diese Registry zeigen, (z. B. `${GRAYLOG_IMAGE:-graylog/graylog:7.1}`) werden mit ihrem Standardwert
werden uebersprungen. aufgeloest. Eintraege mit Variablen *ohne* Standardwert (z. B. `${TAG}`) oder die
bereits auf diese Registry zeigen, werden uebersprungen.
## Images loeschen und Speicher aufraeumen (Web-Oberflaeche) ## Images loeschen und Speicher aufraeumen (Web-Oberflaeche)
+147 -18
View File
@@ -1,6 +1,7 @@
import os import os
import re import re
import secrets import secrets
import shutil
import sqlite3 import sqlite3
import subprocess import subprocess
from functools import wraps from functools import wraps
@@ -26,6 +27,7 @@ ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "changeme")
DB_PATH = os.environ.get("DB_PATH", "/data/users.db") DB_PATH = os.environ.get("DB_PATH", "/data/users.db")
HTPASSWD_PATH = os.environ.get("HTPASSWD_PATH", "/auth/htpasswd") HTPASSWD_PATH = os.environ.get("HTPASSWD_PATH", "/auth/htpasswd")
GC_CONFIG = os.environ.get("GC_CONFIG", "/app/registry-gc-config.yml") GC_CONFIG = os.environ.get("GC_CONFIG", "/app/registry-gc-config.yml")
REGISTRY_DATA = os.environ.get("REGISTRY_DATA", "/var/lib/registry")
REGISTRY_URL = os.environ.get("REGISTRY_URL", "http://registry:5000") REGISTRY_URL = os.environ.get("REGISTRY_URL", "http://registry:5000")
# Host:Port ohne Schema, fuer skopeo-Ziel (z. B. "registry:5000") # Host:Port ohne Schema, fuer skopeo-Ziel (z. B. "registry:5000")
REGISTRY_HOST = REGISTRY_URL.split("://", 1)[-1] REGISTRY_HOST = REGISTRY_URL.split("://", 1)[-1]
@@ -126,12 +128,22 @@ def split_dest(dest):
return repo, tag return repo, tag
# Loest ${VAR:-default} und ${VAR-default} zum Default-Wert auf.
# ${VAR}, ${VAR:?...} usw. (ohne Default) werden NICHT aufgeloest.
_VAR_DEFAULT_RE = re.compile(r"\$\{[A-Za-z_][A-Za-z0-9_]*:?-([^}]*)\}")
def resolve_compose_image(image):
"""Ersetzt Compose-Variablen mit Default (${VAR:-default}) durch den Default."""
return _VAR_DEFAULT_RE.sub(lambda m: m.group(1), image)
def extract_compose_images(content): def extract_compose_images(content):
"""Liest alle 'image:'-Eintraege aus den Services einer docker-compose. """Liest alle 'image:'-Eintraege aus den Services einer docker-compose.
Gibt (images, ignored) zurueck. images ist None bei ungueltigem YAML. Gibt (images, ignored) zurueck. images ist None bei ungueltigem YAML.
Eintraege mit Variablen (${...}) oder auf die eigene Registry werden Variablen mit Default (${VAR:-wert}) werden aufgeloest. Eintraege mit
uebersprungen und in ignored gesammelt. nicht aufloesbaren Variablen oder auf die eigene Registry landen in ignored.
""" """
try: try:
data = yaml.safe_load(content) data = yaml.safe_load(content)
@@ -147,13 +159,14 @@ def extract_compose_images(content):
for svc in services.values(): for svc in services.values():
if not isinstance(svc, dict): if not isinstance(svc, dict):
continue continue
image = svc.get("image") raw = svc.get("image")
if not isinstance(image, str) or not image.strip(): if not isinstance(raw, str) or not raw.strip():
continue continue
image = image.strip() raw = raw.strip()
image = resolve_compose_image(raw)
host = image.split("/", 1)[0] if "/" in image else "" host = image.split("/", 1)[0] if "/" in image else ""
if "$" in image or "{" in image: if not image or "$" in image or "{" in image or "}" in image:
ignored.append(image) # nicht aufgeloeste Variable ignored.append(raw) # Variable ohne Default - nicht aufloesbar
elif host and host in (REGISTRY_HOST, request.host): elif host and host in (REGISTRY_HOST, request.host):
ignored.append(image) # zeigt bereits auf unsere Registry ignored.append(image) # zeigt bereits auf unsere Registry
elif not _IMAGE_REF_RE.match(image): elif not _IMAGE_REF_RE.match(image):
@@ -202,6 +215,104 @@ def delete_manifest(name, digest):
return False return False
def repo_has_tags(name):
"""True, wenn das Repository noch mindestens einen Tag hat."""
data = query_registry(f"/v2/{name}/tags/list")
return bool(data and data.get("tags"))
def _repositories_base():
return os.path.realpath(
os.path.join(REGISTRY_DATA, "docker", "registry", "v2", "repositories")
)
def _safe_repo_path(*parts):
"""Absoluter Pfad innerhalb des repositories-Verzeichnisses oder None (Schutz vor ../)."""
base = _repositories_base()
target = os.path.realpath(os.path.join(base, *parts))
if target != base and not target.startswith(base + os.sep):
return None
return target
def scan_fs_repositories():
"""Listet alle Repositories direkt aus dem Speicher (Dateisystem).
Erfasst auch Repositories, die der Registry-Katalog (/v2/_catalog) nicht
zeigt - etwa nach Korruption, wenn das _layers-Verzeichnis fehlt.
"""
base = _repositories_base()
repos = []
if not os.path.isdir(base):
return repos
for root, dirs, _files in os.walk(base):
if "_manifests" in dirs:
name = os.path.relpath(root, base).replace(os.sep, "/")
if name != ".":
repos.append(name)
dirs[:] = [] # innerhalb eines Repos nicht weiter absteigen
return repos
def repo_tags(name):
"""Tags eines Repositories - aus dem Dateisystem, mit API als Fallback."""
tags_dir = _safe_repo_path(*name.split("/"), "_manifests", "tags")
if tags_dir and os.path.isdir(tags_dir):
return sorted(
d for d in os.listdir(tags_dir) if os.path.isdir(os.path.join(tags_dir, d))
)
data = query_registry(f"/v2/{name}/tags/list")
return sorted(data.get("tags") or []) if data else []
def remove_repository_storage(name):
"""Entfernt das leere Repository-Verzeichnis aus dem Registry-Speicher.
Die Registry-API kennt kein "Repository loeschen"; nach dem Loeschen des
letzten Tags bleibt sonst ein leerer Eintrag im Katalog stehen.
"""
target = _safe_repo_path(*name.split("/"))
if not target or not os.path.isdir(target):
return False
shutil.rmtree(target, ignore_errors=True)
# Leere Eltern-Verzeichnisse (z. B. "bitnami" nach "bitnami/redis") aufraeumen
base = _repositories_base()
parent = os.path.dirname(target)
while parent != base and os.path.isdir(parent) and not os.listdir(parent):
try:
os.rmdir(parent)
except OSError:
break
parent = os.path.dirname(parent)
return True
def remove_tag_storage(name, tag):
"""Entfernt den Tag-Verweis direkt aus dem Speicher.
Noetig fuer verwaiste Tags, die noch im Katalog stehen, deren Manifest
aber nicht mehr abrufbar ist (Loeschen ueber die API schlaegt dann fehl).
"""
target = _safe_repo_path(*name.split("/"), "_manifests", "tags", tag)
if not target or not os.path.isdir(target):
return False
shutil.rmtree(target, ignore_errors=True)
return True
def prune_empty_repositories():
"""Entfernt alle Repositories ohne Tags aus dem Speicher. Gibt Anzahl zurueck."""
data = query_registry("/v2/_catalog")
if not data:
return 0
removed = 0
for name in data.get("repositories", []):
if not repo_has_tags(name) and remove_repository_storage(name):
removed += 1
return removed
def remote_digest(source, src_creds=None, src_token=None): def remote_digest(source, src_creds=None, src_token=None):
"""Liefert den Manifest-Digest eines Quell-Images, ohne es herunterzuladen.""" """Liefert den Manifest-Digest eines Quell-Images, ohne es herunterzuladen."""
cmd = ["skopeo", "inspect", "--format", "{{.Digest}}", f"docker://{source}"] cmd = ["skopeo", "inspect", "--format", "{{.Digest}}", f"docker://{source}"]
@@ -308,13 +419,13 @@ def users():
@app.route("/images") @app.route("/images")
@login_required @login_required
def images(): def images():
repos = [] # Repos aus dem Dateisystem UND dem Katalog (Vereinigung), damit auch
# Repositories sichtbar/loeschbar sind, die der Katalog nicht zeigt.
names = set(scan_fs_repositories())
data = query_registry("/v2/_catalog") data = query_registry("/v2/_catalog")
if data: if data:
for name in sorted(data.get("repositories", [])): names.update(data.get("repositories", []))
tags_data = query_registry(f"/v2/{name}/tags/list") repos = [{"name": name, "tags": repo_tags(name)} for name in sorted(names)]
tags = sorted(tags_data.get("tags", [])) if tags_data and tags_data.get("tags") else []
repos.append({"name": name, "tags": tags})
return render_template("images.html", repos=repos) return render_template("images.html", repos=repos)
@@ -423,14 +534,26 @@ def delete_image():
return redirect(url_for("images")) return redirect(url_for("images"))
digest = get_manifest_digest(name, tag) digest = get_manifest_digest(name, tag)
if not digest: # Normalfall: Manifest per Digest loeschen. Faellt der API-Lookup aus
# (verwaister Tag: im Katalog gelistet, Manifest aber nicht abrufbar),
# den Tag-Verweis direkt aus dem Speicher entfernen.
manifest_deleted = delete_manifest(name, digest) if digest else False
tag_removed = remove_tag_storage(name, tag)
if not manifest_deleted and not tag_removed:
flash(f'Tag "{tag}" von "{name}" wurde nicht gefunden.', "error") flash(f'Tag "{tag}" von "{name}" wurde nicht gefunden.', "error")
return redirect(url_for("images")) return redirect(url_for("images"))
if delete_manifest(name, digest): # War das der letzte Tag? Dann das leere Repository entfernen, damit
flash(f'Image "{name}:{tag}" wurde geloescht.', "success") # es nicht weiter (ohne Tags) im Katalog stehen bleibt.
if not repo_has_tags(name) and remove_repository_storage(name):
flash(
f'Image "{name}:{tag}" geloescht - Repository "{name}" entfernt '
"(keine Tags mehr).",
"success",
)
else: else:
flash(f'Image "{name}:{tag}" konnte nicht geloescht werden.', "error") flash(f'Image "{name}:{tag}" wurde geloescht.', "success")
return redirect(url_for("images")) return redirect(url_for("images"))
@@ -441,9 +564,13 @@ def garbage_collect():
# Entfernt Blobs/Layer, die von keinem Manifest mehr referenziert werden # Entfernt Blobs/Layer, die von keinem Manifest mehr referenziert werden
# (z. B. nach dem Loeschen von Tags). Nutzt die registry-Binary direkt auf # (z. B. nach dem Loeschen von Tags). Nutzt die registry-Binary direkt auf
# dem gemounteten Speicher - kein Docker-Socket noetig. # dem gemounteten Speicher - kein Docker-Socket noetig.
#
# WICHTIG: KEIN --delete-untagged! Dieses Flag loescht bei Multi-Arch-Images
# die per-Architektur-Manifeste (die nur ueber die Manifest-Liste referenziert
# sind) und macht die Images damit unbrauchbar ("manifest unknown" beim Pull).
try: try:
result = subprocess.run( result = subprocess.run(
["registry", "garbage-collect", GC_CONFIG, "--delete-untagged"], ["registry", "garbage-collect", GC_CONFIG],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=600, timeout=600,
@@ -454,9 +581,11 @@ def garbage_collect():
for line in result.stdout.splitlines() for line in result.stdout.splitlines()
if line.startswith("blob eligible for deletion") if line.startswith("blob eligible for deletion")
) )
pruned = prune_empty_repositories()
extra = f" {pruned} leere(s) Repository entfernt." if pruned else ""
flash( flash(
"Garbage Collection abgeschlossen. " "Garbage Collection abgeschlossen. "
f"{freed} nicht mehr benoetigte Blob(s) wurden entfernt.", f"{freed} nicht mehr benoetigte Blob(s) wurden entfernt.{extra}",
"success", "success",
) )
else: else:
+4
View File
@@ -25,6 +25,10 @@
input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); } input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
textarea { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; } textarea { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); } textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 2.5rem; }
.pw-toggle { position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); background: none; border: none; padding: 0.25rem; cursor: pointer; color: #6b7280; display: flex; align-items: center; }
.pw-toggle:hover { color: #374151; }
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; }
.btn-primary { background: #2563eb; color: #fff; } .btn-primary { background: #2563eb; color: #fff; }
.btn-primary:hover { background: #1d4ed8; } .btn-primary:hover { background: #1d4ed8; }
+40 -8
View File
@@ -1,6 +1,27 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Images - Docker Registry{% endblock %} {% block title %}Images - Docker Registry{% endblock %}
{# Passwort-Feld mit Auge zum Ein-/Ausblenden der Eingabe. #}
{% macro pw_field(fid, ph='') %}
<div class="pw-wrap">
<input type="password" id="{{ fid }}" name="{{ fid }}" autocomplete="new-password"
{% if ph %}placeholder="{{ ph }}"{% endif %}>
<button type="button" class="pw-toggle" onclick="togglePw('{{ fid }}', this)"
title="Anzeigen/Verbergen" aria-label="Eingabe anzeigen oder verbergen">
<svg class="eye-on" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-off" style="display: none;" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
{% endmacro %}
{# Wiederverwendbarer Zugangsdaten-Block (Benutzername/Kennwort ODER Access Token). {# Wiederverwendbarer Zugangsdaten-Block (Benutzername/Kennwort ODER Access Token).
prefix unterscheidet die Felder von Einzel- und Bulk-Formular. #} prefix unterscheidet die Felder von Einzel- und Bulk-Formular. #}
{% macro cred_fields(prefix) %} {% macro cred_fields(prefix) %}
@@ -22,7 +43,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ prefix }}src_password">Kennwort</label> <label for="{{ prefix }}src_password">Kennwort</label>
<input type="password" id="{{ prefix }}src_password" name="{{ prefix }}src_password" autocomplete="new-password"> {{ pw_field(prefix ~ 'src_password') }}
</div> </div>
</div> </div>
</div> </div>
@@ -36,8 +57,7 @@
<div id="{{ prefix }}token_box" style="display: none;"> <div id="{{ prefix }}token_box" style="display: none;">
<div class="form-group" style="margin-top: 0.5rem;"> <div class="form-group" style="margin-top: 0.5rem;">
<label for="{{ prefix }}src_token">Access Token</label> <label for="{{ prefix }}src_token">Access Token</label>
<input type="password" id="{{ prefix }}src_token" name="{{ prefix }}src_token" autocomplete="new-password" {{ pw_field(prefix ~ 'src_token', 'Bearer-Token der Quell-Registry') }}
placeholder="Bearer-Token der Quell-Registry">
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -121,9 +141,11 @@
<code>image:</code>-Eintrag gelesen und das Image in diese Registry kopiert. <code>image:</code>-Eintrag gelesen und das Image in diese Registry kopiert.
</p> </p>
<p class="help-step"> <p class="help-step">
Eintraege mit nicht aufgeloesten Variablen (z. B. <code>${TAG}</code>) oder die bereits Variablen mit Standardwert (z. B. <code>${GRAYLOG_IMAGE:-graylog/graylog:7.1}</code>)
auf diese Registry zeigen, werden uebersprungen. Bereits vorhandene Images (gleicher werden automatisch mit ihrem Standardwert aufgeloest. Eintraege mit Variablen
Digest) werden ebenfalls nicht erneut geladen. <em>ohne</em> Standardwert (z. B. <code>${TAG}</code>) oder die bereits auf diese
Registry zeigen, werden uebersprungen. Bereits vorhandene Images (gleicher Digest)
werden ebenfalls nicht erneut geladen.
</p> </p>
<p class="help-step"> <p class="help-step">
Etwaige Zugangsdaten gelten fuer <em>alle</em> Images der Compose-Datei. Etwaige Zugangsdaten gelten fuer <em>alle</em> Images der Compose-Datei.
@@ -186,8 +208,9 @@
<p style="margin-top: 0.6rem; font-size: 0.85rem; color: #6b7280; line-height: 1.6;"> <p style="margin-top: 0.6rem; font-size: 0.85rem; color: #6b7280; line-height: 1.6;">
Beim Loeschen eines Images verschwindet der Tag sofort, der belegte Speicherplatz wird Beim Loeschen eines Images verschwindet der Tag sofort, der belegte Speicherplatz wird
aber erst hierdurch freigegeben. Entfernt alle Daten, die von keinem Image mehr aber erst hierdurch freigegeben. Entfernt alle Daten, die von keinem Image mehr
referenziert werden. <strong>Wichtig:</strong> waehrend der Aufraeumung keine Images referenziert werden, sowie leere Repositories ohne Tags.
hochladen, sonst koennen frisch hochgeladene Daten verloren gehen. <strong>Wichtig:</strong> waehrend der Aufraeumung keine Images hochladen, sonst
koennen frisch hochgeladene Daten verloren gehen.
</p> </p>
</div> </div>
</div> </div>
@@ -213,6 +236,15 @@
return true; return true;
} }
// Auge: Eingabe ein-/ausblenden.
function togglePw(id, btn) {
var inp = document.getElementById(id);
var show = inp.type === 'password';
inp.type = show ? 'text' : 'password';
btn.querySelector('.eye-on').style.display = show ? 'none' : '';
btn.querySelector('.eye-off').style.display = show ? '' : 'none';
}
// Benutzername/Kennwort und Access Token schliessen sich gegenseitig aus. // Benutzername/Kennwort und Access Token schliessen sich gegenseitig aus.
function toggleCredMode(prefix, mode) { function toggleCredMode(prefix, mode) {
var creds = document.getElementById(prefix + 'use_creds'); var creds = document.getElementById(prefix + 'use_creds');
+6
View File
@@ -34,6 +34,12 @@ services:
REGISTRY_AUTH_HTPASSWD_REALM: Docker Registry REGISTRY_AUTH_HTPASSWD_REALM: Docker Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_STORAGE_DELETE_ENABLED: "true" REGISTRY_STORAGE_DELETE_ENABLED: "true"
# Blob-Cache deaktivieren: Die Garbage Collection laeuft als separater
# Prozess auf dem gemounteten Speicher. Mit aktivem In-Memory-Cache wuerde
# die laufende Registry geloeschte Blobs faelschlich fuer vorhanden halten
# und beim erneuten Pull keine Layer-Verknuepfung anlegen (Image fehlt dann
# im Katalog / "manifest unknown"). Ohne Cache ist die Live-GC sicher.
REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: ""
volumes: volumes:
- ./data/registry:/var/lib/registry - ./data/registry:/var/lib/registry
- ./data/htpasswd:/auth:ro - ./data/htpasswd:/auth:ro