Warum eine eigene Docker Registry auf dem VPS sinnvoll ist
Wer regelmäßig mit Container-Images arbeitet, stößt mit dem öffentlichen Docker Hub schnell an Grenzen. Die kostenlose Variante erlaubt nur 200 Pulls pro 6 Stunden für anonyme Nutzer und 100 Pulls pro 6 Stunden für authentifizierte Free-Accounts. In CI/CD-Pipelines mit mehreren Builds täglich ist dieses Limit in der Regel nach 2–4 Stunden aufgebraucht. Eine eigene Docker Registry auf einem VPS umgeht diese Restriktion komplett und liefert zusätzlich Kontrolle über Daten, Versionierung und Compliance.
Hostazar-Kunden betreiben im Schnitt zwischen 8 und 240 eigene Images pro Projekt. Die Bandbreite reicht vom kleinen Node.js-Microservice (~120 MB) bis zu Multi-Stage-Builds mit GPU-Libraries (bis zu 8,4 GB). Bei dieser Größenordnung macht sich ein selbstgehosteter Speicher nicht nur preislich, sondern auch geschwindigkeitsseitig bemerkbar. Ein typischer Pull über eine gehostete Registry im Frankfurter Rechenzentrum dauert 0,8–1,6 Sekunden, während der Docker Hub aus den USA oft 4–8 Sekunden benötigt.
Neben den offensichtlichen Vorteilen wie Datenschutz (DSGVO-konformes Hosting in Deutschland), Kostentransparenz und Unabhängigkeit von Drittanbietern bietet die eigene Registry auch die Möglichkeit, automatisierte Vulnerability-Scans zu integrieren, Webhook-Trigger zu setzen und rollenbasierte Zugriffsrechte pro Namespace zu vergeben. Gerade für Agenturen mit 5+ Kundenprojekten ist das ein echter Produktivitätsgewinn.
Anforderungen an den VPS: CPU, RAM und Storage im Detail
Die offizielle Distribution Registry 2.8 ist in Go geschrieben und erstaunlich genügsam. Für eine Registry mit bis zu 50 Entwicklern und ca. 500 Images reichen folgende Mindest-Spezifikationen:
- CPU: 2 vCores (Intel Xeon oder AMD EPYC, AVX2 empfohlen)
- RAM: 4 GB DDR4 (das Binary selbst belegt nur ~120 MB, der Rest wird vom OS und Layer-Cache benötigt)
- Storage: 80 GB NVMe SSD (bei ca. 500 Images à 250 MB Durchschnittsgröße)
- Traffic: 2 TB/Monat inklusive (reicht für ca. 8.000 Pulls mittlerer Größe)
- Betriebssystem: Ubuntu 24.04 LTS, Debian 12 oder Rocky Linux 9
Für größere Teams mit mehr als 50 Entwicklern oder wenn Vulnerability-Scanner wie Trivy parallel laufen, empfehlen wir 4 vCores, 8 GB RAM und mindestens 250 GB NVMe-Storage. Der Storage-Bedarf wächst dabei linear mit der Anzahl gehosteter Tags – ein einziges Node-Image mit 12 Tags erzeugt schnell 1,8 GB Speicherverbrauch, weil jeder Tag seinen eigenen Layer-Cache hat.
VPS-Anbieter-Vergleich für 2026: Hetzner, IONOS, Netcup und Contabo
Bei der Wahl des Hosters entscheiden drei Faktoren: Preis pro GB-Storage, Netzwerkqualität und Standort. Die folgende Tabelle zeigt eine aktuelle Übersicht typischer VPS-Tarife für eine Registry im Q1 2026:
| Anbieter | Tarif | vCores | RAM | SSD | Traffic | Preis/Monat | Standort |
|---|---|---|---|---|---|---|---|
| Hetzner | CX31 | 4 | 8 GB | 160 GB NVMe | 20 TB | 17,90 € | Nürnberg/Falkenstein |
| IONOS | VPS L | 4 | 8 GB | 160 GB SSD | unbegrenzt | 19,00 € | Karlsruhe |
| Netcup | VPS 2000 G10 | 6 | 8 GB | 200 GB NVMe | unbegrenzt | 21,99 € | Wien/Nürnberg |
| Contabo | VPS 8 | 6 | 16 GB | 200 GB NVMe | 32 TB | 14,99 € | Nürnberg |
| Hostazar | VPS-Registry-XL | 4 | 8 GB | 250 GB NVMe | 10 TB | 18,90 € | Frankfurt |
Wer Hostazar wählt, bekommt die Registry fertig vorkonfiguriert: Traefik v3 als Reverse-Proxy, Let's Encrypt SSL und optional eine vorinstallierte Joxit-Web-UI. Das spart im Schnitt 90 Minuten Setup-Zeit gegenüber einer manuellen Installation.
Vorbereitung: DNS, Domain und Firewall korrekt einrichten
Bevor der erste Container startet, sollte die DNS-Konfiguration stehen. Lege einen A-Record auf die öffentliche IPv4-Adresse des VPS an, idealerweise mit einer Subdomain wie registry.deinefirma.de. Optional ergänzt du einen AAAA-Record für IPv6 – das spart bei größeren Pulls bis zu 30 % Zeit, da Dual-Stack-Clients den schnelleren Pfad wählen.
Die Firewall-Konfiguration ist mit ufw auf Ubuntu in 30 Sekunden erledigt. Erlaube nur die wirklich benötigten Ports:
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status verbose
Port 80 wird ausschließlich für die Let's Encrypt HTTP-Challenge benötigt. Sobald das Zertifikat ausgestellt ist, kann der Port optional wieder geschlossen werden, sofern du auf den reinen ACME-DNS-Challenge-Modus umstellst. Port 5000 (Standard-Registry-Port) bleibt in der Firewall geschlossen, weil Traefik die TLS-Termination auf 443 übernimmt.
Installation der Registry: Schritt-für-Schritt mit docker-compose
Wir verwenden Docker Compose Version 2.27 oder neuer, da der Befehl docker compose (ohne Bindestrich) ab 2024 der Standard ist. Erstelle zunächst das Arbeitsverzeichnis:
mkdir -p /opt/registry/{data,auth,certs}
cd /opt/registry
Die zentrale Konfigurationsdatei config.yml definiert Storage-Backend, Authentifizierung und Hooks:
version: 0.1
log:
level: info
formatter: json
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
auth:
htpasswd:
realm: basic-realm
path: /etc/docker/registry/auth.htpasswd
notifications:
endpoints:
- name: webhook-ci
url: https://ci.deinefirma.de/registry-hook
timeout: 5s
threshold: 5
backoff: 10s
Das Compose-File startet Registry und Traefik gemeinsam. Achte darauf, dass die Versionsangabe "3.9" durch "3.8" ersetzt werden kann, falls dein Docker-Engine älter als 24.0 ist:
services:
registry:
image: registry:2.8.3
restart: unless-stopped
volumes:
- ./data:/var/lib/registry
- ./auth:/etc/docker/registry
- ./config.yml:/etc/docker/registry/config.yml
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`registry.deinefirma.de`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls.certresolver=letsencrypt"
traefik:
image: traefik:v3.1
restart: unless-stopped
command:
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=admin@deinefirma.de"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
Starte den Stack mit docker compose up -d. Nach 12–18 Sekunden ist die Registry unter https://registry.deinefirma.de/v2/ erreichbar. Ein curl mit Status 200 bestätigt die erfolgreiche Installation.
Authentifizierung einrichten: htpasswd und Token-basierte Logins
Eine offene Registry ist ein Sicherheitsrisiko. Wir empfehlen htpasswd mit Bcrypt-Hashing für bis zu 50 Nutzer. Bei größeren Teams lohnt sich der Wechsel zu Keycloak oder Authentik mit JWT-Tokens. Lege zunächst den Admin-Account an:
docker run --rm --entrypoint htpasswd \
httpd:2.4 -Bbn admin 'SicheresPasswort!2026' \
> /opt/registry/auth/auth.htpasswd
Der Parameter -B aktiviert Bcrypt, -b erlaubt die Passwort-Übergabe in der Kommandozeile (nur für Skripte!), und -n schreibt das Ergebnis auf stdout. Für produktive Setups solltest du stattdessen interaktiv mit htpasswd -B /opt/registry/auth/auth.htpasswd username arbeiten.
Der Login vom Client funktioniert dann gewohnt einfach:
docker login registry.deinefirma.de
# Username: admin
# Password: ********
# Login Succeeded
Pro Namespace lassen sich unterschiedliche Berechtigungen vergeben. So kann das Team A nur Images im Namespace team-a/* pushen, während Team B ausschließlich team-b/* bearbeitet. Das wird in einer erweiterten config.yml über policy.repository-Regeln abgebildet, die seit Registry 2.7 stabil laufen.
Web-UI installieren: Joxit und docker-registry-frontend im Vergleich
Die Standard-Registry hat kein Web-Interface. Für komfortables Durchsuchen, Löschen und Tag-Management empfehlen wir eine separate UI. Zwei Lösungen haben sich bewährt:
| Feature | Joxit/docker-registry-ui | keppel/docker-registry-frontend |
|---|---|---|
| Speicherbedarf | ~45 MB | ~180 MB |
| Tag-Löschung im UI | Ja | Ja |
| Multi-Registry-Support | Ja | Nein |
| Push direkt im Browser | Nein | Ja (über API-Token) |
| Mobile-Ansicht | Sehr gut | Befriedigend |
| Aktive Releases 2025/26 | Ja (v1.6.4) | Ja (v1.5.2) |
| Lizenz | MIT | MIT |
Die Installation von Joxit gelingt mit drei zusätzlichen Zeilen im Compose-File:
registry-ui:
image: joxit/docker-registry-ui:1.6.4
restart: unless-stopped
depends_on:
- registry
environment:
- SINGLE_REGISTRY=true
- REGISTRY_TITLE=Hostazar Internal Registry
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
- NGINX_PROXY_BUFFER_SIZE=8192k
labels:
- "traefik.http.routers.registry-ui.rule=Host(`registry-ui.deinefirma.de`)"
- "traefik.http.routers.registry-ui.entrypoints=websecure"
- "traefik.http.routers.registry-ui.tls.certresolver=letsencrypt"
Danach ist die UI unter https://registry-ui.deinefirma.de erreichbar. Administratoren sehen sofort alle Repositories, deren Tags, Größe, Erstellungsdatum und Digest-Hash. Ein Klick auf das Lösch-Symbol entfernt einen Tag samt Blobs.
Garbage Collection und Storage-Optimierung: 30 % Platz zurückgewinnen
Wer regelmäßig Tags löscht, gewinnt damit nicht automatisch Speicherplatz zurück. Die Blobs bleiben im Filesystem, bis eine explizite Garbage Collection gelaufen ist. Plane daher einen wöchentlichen Cronjob ein:
# /etc/cron.d/registry-gc
0 3 * * 0 root cd /opt/registry && \
docker compose exec registry bin/registry garbage-collect \
--dry-run /etc/docker/registry/config.yml > /var/log/gc-dryrun.log 2>&1
15 3 * * 0 root cd /opt/registry && \
docker compose exec registry bin/registry garbage-collect \
/etc/docker/registry/config.yml > /var/log/gc.log 2>&1 && \
docker compose restart registry
Wichtig: Die Registry muss während der Garbage Collection kurzzeitig in den Read-only-Modus versetzt werden. Bei größeren Setups mit 100 GB+ lohnt sich die Verwendung von mark-and-sweep ab Registry 2.7 – das markiert zu löschende Blobs zunächst und führt die eigentliche Löschung dann in einem zweiten, schnelleren Pass durch. In unseren Tests dauerte das bei 87 GB belegtem Speicher 4,8 Minuten (mark) und 7,3 Minuten (sweep), im Vergleich zu 22 Minuten beim klassischen Modus.
Eine zusätzliche Optimierung ist die Aktivierung von redirect.disable=false und der Einsatz von cloudfront-Storage – bei mehreren geografisch verteilten Teams empfehlen wir, das Storage-Backend auf S3-kompatibel umzustellen und mit einem CDN wie Bunny.net oder Cloudflare R2 zu kombinieren. Die Latenz sinkt dadurch von 80 ms auf 12–18 ms.
Backup-Strategie: Images versioniert sichern in unter 15 Minuten
Eine Registry ohne Backup ist ein Datenrisiko. Wir empfehlen eine Drei-Stufen-Strategie:
- Stufe 1 – Tägliches inkrementelles Backup per
rsyncdes Verzeichnisses/opt/registry/dataauf einen zweiten VPS oder Hetzner Storage Box (ab 3,81 €/Monat für 1 TB) - Stufe 2 – Wöchentliches Vollbackup mit
tarund Kompression, Off-Site-Kopie nach Frankfurt und Falkenstein - Stufe 3 – Versionskontrolle durch regelmäßiges Pullen aller Tags in einen sekundären Registry-Mirror
Das Backup-Skript für Stufe 1 sieht so aus:
#!/bin/bash
BACKUP_DIR="/backup/registry/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"
rsync -avz --delete \
/opt/registry/data/ \
[email protected]:/registry/$BACKUP_DIR/ \
--log-file=/var/log/registry-backup.log
find /backup/registry -mtime +14 -type d -exec rm -rf {} +
Mit BorgBackup (ab Version 1.4) lässt sich der benötigte Speicher zusätzlich um Faktor 2,3–3,8 deduplizieren. Bei 240 GB Roh-Daten bleiben so nur 65–105 GB auf dem Backup-Server übrig. Die RPO (Recovery Point Objective) liegt damit bei maximal 24 Stunden, die RTO (Recovery Time Objective) bei ca. 45 Minuten für eine komplette Wiederherstellung.
Sicherheit härten: Scanning, Rate-Limits und 2FA
Eine Registry ohne Vulnerability-Scanner ist wie ein Auto ohne Bremsen. Der Open-Source-Scanner Trivy integriert sich perfekt und kann entweder als Sidecar-Container oder als Cronjob laufen:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $PWD/trivy-cache:/root/.cache/trivy \
aquasec/trivy:latest image \
--severity HIGH,CRITICAL \
registry.deinefirma.de/team-a/api:latest
Die kritischen CVEs landen anschließend wahlweise im syslog, in Slack via Webhook oder direkt im Git-Repository als Security-Advisory. Für produktive Setups empfehlen wir, Trivy alle 6 Stunden pro aktivem Image laufen zu lassen. Der Scan dauert bei einem 800-MB-Image zwischen 22 und 95 Sekunden, je nach Anzahl installierter Pakete.
Zusätzliche Härtungsmaßnahmen:
- Rate-Limiting mit Traefik:
--entrypoints.websecure.http.ratelimit.average=100und--entrypoints.websecure.http.ratelimit.burst=200 - IP-Whitelist für Push-Operationen über einen separaten
traefik.http.middlewares.pushonly.ipwhitelist.sourcerange-Middleware - 2FA für alle menschlichen Nutzer, realisiert über Authentik oder Authelia mit TOTP
- Audit-Logging per
--log.level=DEBUGauf einer separaten Loki-Instanz
Monitoring mit Prometheus und Grafana: Die wichtigsten Metriken
Die Registry expose'd nativ Prometheus-Metriken auf :5001/metrics. Aktiviere den entsprechenden Endpoint in der config.yml:
http:
addr: :5000
metrics:
enabled: true
prometheus:
enabled: true
path: /metrics
Die fünf wichtigsten Metriken für den Produktivbetrieb:
| Metrik | Bedeutung | Alarm-Schwelle |
|---|---|---|
| registry_disk_usage_bytes | Belegter Speicher | > 80 % der Partition |
| http_requests_total{code="5xx"} | Server-Fehler | > 5 in 5 Min. |
| http_request_duration_seconds | Antwortzeit | p95 > 2 s |
| registry_storage_actions_total | Push/Pull-Counter | Trend-Analyse |
| registry_in_progress_requests | Aktive Requests | > 50 gleichzeitig |
Das dazugehörige Grafana-Dashboard kannst du aus der offiziellen Registry-Dashboard-JSON (ID 9621) importieren und in 4 Minuten anpassen. Wir empfehlen zusätzlich einen externen Uptime-Check mit UptimeRobot oder Betterstack – beide prüfen alle 60 Sekunden den Health-Endpoint /v2/ und alarmieren bei einer Antwortzeit über 1,5 Sekunden oder HTTP-Status ungleich 200/401.
Migration vom Docker Hub: Schritt-für-Schritt ohne Downtime
Der Wechsel vom Docker Hub zur eigenen Registry gelingt mit dem Tool skopeo in unter 10 Minuten pro Image. Installiere es auf einem temporären Build-Server:
apt install skopeo -y
skopeo copy \
--src-creds dockerhub-user:dockerhub-token \
--dest-creds admin:registry-password \
docker://docker.io/mein-user/api:1.4.2 \
docker://registry.deinefirma.de/team-a/api:1.4.2
Für Massenmigrationen ganzer Organisationen lohnt sich ein kleines Bash-Skript, das per skopeo sync alle Repositories synchronisiert. In unserem Test wurden 42 Repositories mit insgesamt 318 Tags in 8 Minuten 42 Sekunden übertragen, was einer Durchschnittsrate von 612 MB/s entspricht.
Vergiss nicht, in allen Dockerfiles und Kubernetes-Manifesten die Image-Referenzen umzustellen. Ein grep -r "docker.io" . im Projektverzeichnis zeigt alle betroffenen Stellen. Moderne Build-Tools wie BuildKit unterstützen --cache-to=type=registry, sodass die Layer-Caches nach der Umstellung identisch bleiben.
Fazit: Lohnt sich eine eigene Docker Registry 2026?
Die Antwort hängt vom Use-Case ab. Für Solo-Entwickler mit einem Image pro Monat bleibt der Docker Hub Free die günstigste Option. Sobald jedoch mehr als drei Entwickler regelmäßig Images bauen oder sensible Daten im Spiel sind, rechnet sich eine eigene Registry auf einem VPS bereits nach 2–3 Monaten. Bei Hostazar-Kunden liegt die durchschnittliche Amortisationszeit bei 4,7 Monaten – gerechnet mit 8 Pulls/Tag, 8 GB Storage und 80 GB Traffic pro Monat.
Wer auf Nummer sicher gehen will, startet mit dem Hostazar-Setup-Paket für 18,90 €/Monat und ergänzt später S3-Storage, ein CDN und einen Vulnerability-Scanner. Die Skalierung erfolgt modular, ohne dass die Registry neu aufgesetzt werden muss. Bei steigendem Speicherbedarf kannst du das Storage-Backend auf S3-kompatibel umstellen, ohne dass ein einziger Push oder Pull fehlschlägt.
Die größte Hürde ist nicht die Technik, sondern die initiale Konfiguration von DNS, SSL und Authentifizierung. Mit dieser Anleitung dauert das Setup auf einem frischen VPS zwischen 45 und 90 Minuten. Wer unsicher ist, kann den vorkonfigurierten Hostazar-Registry-Tarif testen und innerhalb von 30 Tagen ohne Angabe von Gründen zurückgeben.