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:

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:

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:

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:

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:

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.