Kurz gesagt: Traefik begrenzt seit Version 2.11.2 das Lesen einer Anfrage standardmäßig auf 60 Sekunden – einschließlich des Inhalts. Ein docker push mit großen Image-Schichten dauert länger und bricht mit HTTP 499 und „invalid content range" ab. Die Lösung: transport.respondingTimeouts.readTimeout am Einstiegspunkt der Registry anheben – so weit wie nötig, denn der Wert gilt für alle Dienste an diesem Einstiegspunkt.
Warum wir eine eigene Registry betreiben
Unsere Container laufen in einem eigenen Docker-Swarm, verwaltet mit Portainer. Die Images dafür bauen wir selbst. Sie bei einem externen Anbieter abzulegen, nur um sie danach wieder ins eigene Rechenzentrum zu holen, wäre ein Umweg: Der Datenverkehr im Rechenzentrum ist nicht kostenlos, und es gibt eine Abhängigkeit mehr, ohne dass sie etwas bringt. Deshalb betreiben wir eine eigene Container-Registry, aus der Portainer die Images direkt bezieht.
Das lief lange unauffällig – unsere Images sind normalerweise klein, ein Upload dauert Sekunden. Bis wir ein Image mit mehreren Gigabyte hochladen wollten – für das Training eines KI-Modells in Azure. Der Push brach ab, immer wieder. Hier beschreiben wir den Aufbau, den Fehler und die Lösung.
Der Aufbau: HAProxy außen, Traefik innen
Vor allen unseren Diensten steht ein HAProxy. Er nimmt die Verbindungen aus dem Internet an und beendet die TLS-Verschlüsselung. Damit wir nicht für jeden neuen Dienst die HAProxy-Konfiguration anfassen müssen, reicht HAProxy die Anfragen an einen Traefik im Swarm weiter. Traefik liest die Labels der Docker-Dienste aus und leitet anhand des Hostnamens an den richtigen Container. Ein neuer Dienst braucht so nur ein paar Labels – der HAProxy bleibt unverändert.
flowchart LR
I["Internet"] -->|"HTTPS"| H["HAProxy<br/>TLS-Ende"]
H -->|"HTTP, ein Backend"| T["Traefik<br/>Einstiegspunkt für HAProxy"]
T -->|"per Label"| R["Registry<br/>Port 5000"]
T -->|"per Label"| W["weitere Dienste"]
R --- V[("Volume<br/>Image-Daten")]
P["Portainer / Swarm"] -.->|"zieht Images"| H
Warum zwei Proxys?
Ehrlich gesagt: Dem HAProxy vertrauen wir seit Jahren, Traefik finden wir für Docker genial – ihn aber direkt ins Internet zu stellen, fühlte sich nie richtig an. Beim genaueren Hinsehen gibt es für dieses Bauchgefühl durchaus nachvollziehbare Gründe:
- Angriffsfläche. Traefik braucht Zugriff auf die Docker-API, um die Dienste zu erkennen. Wird ein Proxy mit diesem Zugriff angegriffen, steht im schlimmsten Fall der ganze Server offen. Der HAProxy am Rand braucht keinen solchen Zugriff.
- Nachvollziehbarkeit. Die Konfiguration des HAProxy ist statisch und steht an einer Stelle. Bei Traefik ergibt sich das Routing aus den Labels, die jeder Dienst selbst mitbringt. Das ist bequem, macht es aber schwerer, auf einen Blick zu sehen, was von außen erreichbar ist.
- Verhalten bei Updates. Wie sich unten zeigt, hat Traefik einen wichtigen Standardwert in einer kleineren Version geändert. Am Rand des Netzes wollen wir möglichst wenig Überraschungen.
Die Aufteilung verbindet beides: Der HAProxy kümmert sich um TLS und den Schutz am Rand, Traefik um die bequeme Verteilung im Inneren.
In der Traefik-Konfiguration sieht das so aus (gekürzt):
command:
- "--entrypoints.haproxy.address=:80"
- "--providers.swarm.endpoint=unix:///var/run/docker.sock"
- "--providers.swarm.exposedbydefault=false"
- "--providers.swarm.network=traefik-proxy"
Mit exposedbydefault=false ist ein Dienst erst dann erreichbar, wenn er sich ausdrücklich per Label freischaltet. Das verhindert, dass versehentlich interne Dienste nach außen gelangen.
Die Registry selbst
Als Registry nutzen wir das offizielle Image registry:3 der CNCF Distribution. Die relevanten Teile:
services:
registry:
image: registry:3
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: "Registry"
REGISTRY_AUTH_HTPASSWD_PATH: /run/secrets/registry_htpasswd
REGISTRY_HTTP_RELATIVEURLS: "true"
secrets:
- registry_htpasswd
volumes:
- registry-data:/var/lib/registry
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`registry.example.com`)"
- "traefik.http.routers.registry.entrypoints=haproxy"
- "traefik.http.services.registry.loadbalancer.server.port=5000"
Drei Details, die sich lohnen:
- Passwörter als Docker-Secret. Die htpasswd-Datei liegt nicht im Image und nicht in der Stack-Datei, sondern als Secret im Swarm. Die Registry akzeptiert darin übrigens nur bcrypt-Hashes. Und weil Basic Auth die Zugangsdaten im HTTP-Header mitschickt, gehört davor zwingend TLS – bei uns übernimmt das der HAProxy.
- Relative URLs. Die Registry schickt beim Hochladen Weiterleitungen auf die nächste Upload-Adresse. Da sie selbst nur unverschlüsseltes HTTP sieht, würde sie ohne diese Einstellung
http://-Adressen zurückgeben, die von außen nicht funktionieren. MitREGISTRY_HTTP_RELATIVEURLSüberlässt sie das Auflösen dem Client. - Das Volume gehört an einen festen Ort. Ein lokales Volume existiert nur auf dem Server, auf dem es angelegt wurde. Startet der Swarm die Registry nach einem Ausfall auf einem anderen Server, findet sie dort ein leeres Volume vor – die Images scheinen verschwunden. Bei mehreren Servern im Swarm gehört deshalb eine Platzierungsregel in den Dienst, die ihn an den Server mit den Daten bindet, oder die Daten liegen auf gemeinsamem Speicher.
Das Symptom: Der Push bricht ab
Kleine Images ließen sich problemlos hochladen. Bei unserem Trainings-Image mit einer Schicht von rund 5,8 GB brach der Push jedes Mal ab. Docker meldete „invalid content range", im Zugriffsprotokoll von Traefik stand HTTP 499 – der Code für eine Verbindung, die vorzeitig geschlossen wurde.
Unser erster Gedanke war die Größe. Wir haben die große Schicht deshalb in sieben kleinere mit höchstens 1,1 GB aufgeteilt. Das half nicht. Im Nachhinein ist klar, warum: Auch die kleineren Schichten brauchten noch länger als eine Minute.
Die Ursache: 60 Sekunden für die ganze Anfrage
Docker lädt jede Schicht eines Images in einer einzigen, langen HTTP-Anfrage hoch. Traefik begrenzt seit Version 2.11.2 die Zeit, die das Lesen einer eingehenden Anfrage dauern darf, standardmäßig auf 60 Sekunden – und zwar für die gesamte Anfrage einschließlich des Inhalts. Vorher gab es keine Grenze. Dauert der Upload einer Schicht länger, schneidet Traefik ihn nach einer Minute ab.
Das ist kein Fehler, sondern ein sinnvoller Schutz. Bei normaler HTTP-Kommunikation – eine Webseite, ein API-Aufruf, ein Formular – ist eine Anfrage nach Sekundenbruchteilen vollständig angekommen. Eine Anfrage, die nach einer Minute noch nicht fertig ist, ist fast immer entweder hängen geblieben oder Absicht. Genau das nutzen sogenannte „Low and slow"-Angriffe aus:
- Slowloris öffnet viele Verbindungen und schickt die Kopfzeilen der Anfrage quälend langsam, Stück für Stück. Jede Verbindung bleibt offen, bis der Server keine neuen mehr annehmen kann.
- R.U.D.Y. („R U Dead Yet?") macht dasselbe mit dem Inhalt einer Anfrage, etwa einem Formular, das Zeichen für Zeichen übertragen wird.
Solche Angriffe brauchen kaum Bandbreite und sind deshalb schwer zu erkennen. Ein Timeout für die gesamte Anfrage einschließlich des Inhalts setzt ihnen eine klare Grenze. Nur ein Upload von mehreren Gigabyte passt eben nicht in dieses Muster.
Die Lösung: ein längerer Timeout am richtigen Einstiegspunkt
Der Timeout lässt sich nicht pro Dienst setzen, sondern nur pro Einstiegspunkt. Wir haben ihn deshalb an dem Einstiegspunkt erhöht, über den der HAProxy anliefert:
- "--entrypoints.haproxy.transport.respondingTimeouts.readTimeout=30m"
Danach lief der Push ohne Probleme durch.
Wichtig ist, was das bedeutet: Der längere Timeout gilt für alle Dienste hinter diesem Einstiegspunkt, nicht nur für die Registry. Wer das vermeiden will, kann der Registry einen eigenen Einstiegspunkt auf einem eigenen Port geben und nur diesen großzügig einstellen. Den Wert ganz auf null zu setzen, würden wir nicht empfehlen – dann fehlt jeder Schutz gegen hängende Verbindungen.
Und der HAProxy?
Am HAProxy mussten wir nichts ändern. Das ist kein Zufall, sondern liegt daran, wie HAProxy seine Timeouts misst:
timeout clientundtimeout serversind Inaktivitäts-Timeouts. Sie greifen nur, wenn eine Zeit lang gar keine Daten fließen. Ein Upload, der stetig Daten sendet, löst sie nicht aus, egal wie lange er dauert.timeout http-requestbegrenzt die Zeit für eine vollständige Anfrage – standardmäßig aber nur für die Kopfzeilen, nicht für den Inhalt. Für den Inhalt gilt er nur, wenn zusätzlichoption http-buffer-requestgesetzt ist.
In unserer Kette übernimmt damit der HAProxy am Rand den Schutz gegen langsame Kopfzeilen, während Traefik innen großzügig sein darf. Wer HAProxy so konfiguriert hat, dass er Anfragen vollständig puffert, muss dort allerdings ebenfalls nachsehen.
Zwei Sicherheitshinweise zu Traefik
Beim Durchsehen unserer Konfiguration sind uns zwei Punkte aufgefallen, die in vielen Anleitungen so stehen und die man trotzdem nicht ungeprüft übernehmen sollte:
- Das Dashboard im „insecure"-Modus ist ohne Anmeldung erreichbar und zeigt die komplette Routing-Konfiguration. Traefik selbst rät davon ab, es im Produktivbetrieb offen zu betreiben. Mindestens muss der Port per Firewall auf das interne Netz beschränkt sein, besser ist ein eigener Router mit Anmeldung. Bei uns ist Traefik insgesamt nur intern erreichbar – auch das ist ein Vorteil der Aufteilung: Von außen sieht man nur den HAProxy.
- Der Docker-Socket gibt Traefik Zugriff auf die Docker-API – und damit im Ernstfall auf den ganzen Server. Das Einbinden mit
:roändert daran wenig, denn es schützt nur die Socket-Datei, nicht die API dahinter. Traefik empfiehlt selbst unter anderem einen vorgeschalteten Socket-Proxy, der nur die benötigten, lesenden Aufrufe durchlässt.
Fazit
Eine eigene Registry hinter HAProxy und Traefik ist schnell aufgesetzt und spart Wege und Traffic. Man sollte aber wissen, dass Traefiks Standard-Timeout von 60 Sekunden für die ganze Anfrage gilt – ein sinnvoller Schutz für normale Webanfragen, aber zu knapp für große Uploads. Wer große Images hochlädt, muss ihn am passenden Einstiegspunkt anheben, so weit wie nötig und nicht weiter – und sollte wissen, dass er damit für alle Dienste dahinter gilt.
Sie betreiben Container im eigenen Rechenzentrum oder überlegen, Ihre Infrastruktur neu aufzustellen? Mehr zu unseren IT-Services – oder sprechen Sie uns direkt an.
Quellen: Traefik – EntryPoints: respondingTimeouts · Traefik – Migration v2.11 (readTimeout 60 s) · Traefik – Docker-Swarm-Provider und Docker-API-Zugriff · Traefik – API und Dashboard · Traefik Community – Registry-Uploads mit HTTP 499 hinter Traefik · CNCF Distribution – Konfiguration der Registry · HAProxy – Konfigurationshandbuch (timeout client, timeout http-request) · Wikipedia – Slowloris und R.U.D.Y.