Docker Build Context: warum der Build langsam ist und Compose die Datei nicht findet

Docker Build Context: warum der Build langsam ist und Compose die Datei nicht findet

Docker Build Context: warum der Build langsam ist und Compose die Datei nicht findet

Artikel-Übersicht

Zwei Docker-Begriffe heißen fast gleich und meinen Verschiedenes: Der Build Context ist das, was beim Bauen an den Daemon übertragen wird. docker context legt fest, auf welchem Daemon Befehle überhaupt laufen. Diese Verwechslung ist der Ausgangspunkt dieses Artikels. Danach klären wir, warum ein Build vierzig Sekunden braucht, bevor die erste Anweisung ausgeführt wird, was in eine .dockerignore gehört und wie sie sich von .gitignore unterscheidet, und wie man den Build Context in Compose setzt — besonders im Monorepo, wo mehrere Dienste sich ein Verzeichnis teilen. Dazu kommen mehrstufige Builds mit der Klarstellung, was sie verkleinern und was nicht, ein Blick auf Dockers eigene Laufzeit für Sprachmodelle, und zwei vollständige Compose-Dateien aus dem KI-Umfeld.

Kein Einsteigerkurs: Grundkenntnisse in Docker werden vorausgesetzt. Wie sich Docker zu Kubernetes verhält, steht in Kubernetes-Architektur verstehen.

Was der Build Context ist

Der Build Context ist das Verzeichnis, das beim Bauen vollständig an den Docker-Daemon übertragen wird — bevor die erste Anweisung im Dockerfile ausgeführt wird.

docker build -t meineanwendung .

Der Punkt am Ende ist der Build Context. Er sieht aus wie eine Nebensächlichkeit und ist das Thema dieses Artikels.

Aus dieser einen Tatsache folgt alles Weitere:

Warum COPY ../datei nicht funktioniert. Das Dockerfile kann nur auf Dateien innerhalb des Contexts zugreifen. Was außerhalb liegt, wurde nie übertragen und existiert für den Build nicht.

Warum der Build langsam startet. Vor der ersten Anweisung wird das gesamte Verzeichnis eingesammelt und übertragen. Bei einem Projekt mit Abhängigkeitsverzeichnis und Versionshistorie sind das schnell mehrere hundert Megabyte.

Warum die Zwischenspeicherung nicht greift. Ändert sich irgendetwas im Context, kann das die Wiederverwendung zwischengespeicherter Schichten verhindern — auch wenn die geänderte Datei für das Ergebnis irrelevant ist.

Warum der Build langsam ist

Die Ausgabe verrät es, wenn man hinsieht:

=> [internal] load build context
=> => transferring context: 412.83MB

Vierhundert Megabyte, bevor irgendetwas gebaut wird. Die üblichen Verdächtigen:

Übeltäter Typische Größe Gehört in den Build?
Abhängigkeitsverzeichnisse (node_modules, venv, target) 100–800 MB nein — wird im Container neu installiert
.git 10–500 MB nein
Build-Artefakte aus lokalen Läufen 10–200 MB nein
Testdaten, Beispieldateien beliebig selten
Protokolldateien wächst nie
Editor- und Systemdateien klein, aber unnötig nein

Das Abhängigkeitsverzeichnis ist der häufigste Fall, und es ist doppelt schädlich: Es wird übertragen, und wenn es dann noch versehentlich mitkopiert wird, überschreibt es die im Container installierte, für die Zielplattform passende Fassung.

.dockerignore richtig schreiben

Die Datei liegt im Wurzelverzeichnis des Build Contexts und wirkt genau dort:

# Abhängigkeiten — werden im Container installiert
node_modules/
venv/
target/
.gradle/

# Versionsverwaltung
.git/
.gitignore

# Build-Artefakte
dist/
build/
*.pyc
__pycache__/

# Entwicklungsumgebung
.idea/
.vscode/
.DS_Store

# Protokolle und lokale Konfiguration
*.log
.env
.env.local

# Docker selbst
Dockerfile*
compose*.yaml

Zwei Punkte, die überraschen:

Die Syntax ist nicht die von .gitignore. Sie ähnelt ihr, folgt aber anderen Regeln — insbesondere bei der Behandlung von Verzeichnispfaden und beim Zusammenspiel von Ausschlüssen und Wiedereinschlüssen mit !. Wer eine .gitignore einfach kopiert, bekommt oft ein anderes Ergebnis als erwartet.

Auch .env sollte drin stehen. Eine Datei mit Zugangsdaten hat in einem Abbild nichts zu suchen — und wer sie im Context lässt, riskiert, dass sie durch ein weit gefasstes COPY . . ins Abbild wandert und dort dauerhaft in einer Schicht liegt. Selbst wenn eine spätere Anweisung sie löscht, bleibt sie in der Historie des Abbilds lesbar.

Ob man mit einer Verbots- oder einer Erlaubnisliste arbeitet, ist Geschmackssache. Die Verbotsliste oben ist leichter zu pflegen; eine Erlaubnisliste (* ausschließen, dann gezielt wieder einschließen) ist sicherer, weil versehentlich hinzugefügte Dateien nicht automatisch mitwandern.

Build Context in Compose

Standardmäßig ist der Context das Verzeichnis der Compose-Datei. Interessant wird es, wenn das nicht passt:

services:
  api:
    build:
      context: .                     # was übertragen wird
      dockerfile: docker/api.Dockerfile   # wo das Dockerfile liegt
    ports:
      - "8080:8080"

context und dockerfile sind getrennt — das Dockerfile muss nicht im Context liegen.

Der Monorepo-Fall

Mehrere Dienste in einem Repository, mit gemeinsamem Code:

services:
  api:
    build:
      context: .                          # Wurzel, damit shared/ erreichbar ist
      dockerfile: services/api/Dockerfile
  worker:
    build:
      context: .
      dockerfile: services/worker/Dockerfile
  frontend:
    build:
      context: ./services/frontend        # eigener Context, braucht shared/ nicht

Die ersten beiden Dienste brauchen die Wurzel als Context, weil ihre Dockerfiles auf shared/ zugreifen. Der dritte kommt mit einem kleineren Context aus — und baut dadurch spürbar schneller.

Der Preis der Wurzel als Context: Jede Änderung irgendwo im Repository kann die Zwischenspeicherung beider Dienste ungültig machen. Eine sorgfältige .dockerignore ist hier keine Kür, sondern die Voraussetzung für erträgliche Bauzeiten.

docker context ist etwas anderes

Und jetzt der Begriff, der die ganze Verwirrung verursacht.

docker context legt fest, mit welchem Docker-Daemon deine Befehle sprechen. Mit dem Build Context hat das nichts zu tun außer dem Namen.

docker context ls
docker context create server --docker "host=ssh://benutzer@server.example.com"
docker context use server
docker ps            # zeigt jetzt die Container auf dem entfernten Server

Anwendungsfälle: auf einem entfernten Server bauen, weil er mehr Ressourcen hat; zwischen lokaler Umgebung und Testsystem wechseln; auf einer virtuellen Maschine arbeiten.

Die Merkregel: Build Context = was übertragen wird. Docker Context = wohin es übertragen wird.

Mehrstufige Builds

Ein häufiges Missverständnis: Mehrstufige Builds verkleinern nicht den Build Context. Der wird trotzdem vollständig übertragen. Was sie verkleinern, ist das Ergebnis.

# Stufe 1: bauen — mit allen Werkzeugen
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stufe 2: ausliefern — nur das Ergebnis
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

Das Endabbild enthält weder die Werkzeugkette noch die Abhängigkeiten noch den Quellcode — nur das gebaute Ergebnis. Aus mehreren hundert Megabyte werden wenige Dutzend.

Der zweite Gewinn ist die Zwischenspeicherung: COPY package*.json vor COPY . . sorgt dafür, dass die Abhängigkeiten nur neu installiert werden, wenn sich die Abhängigkeitsdatei ändert — nicht bei jeder Codeänderung. Diese Reihenfolge ist der wirksamste einzelne Griff für schnelle Builds.

Docker Model Runner

Docker hat inzwischen eine eigene Laufzeit für Sprachmodelle: Modelle werden wie Abbilder verwaltet und über eine OpenAI-kompatible Schnittstelle bereitgestellt.

Die naheliegende Frage ist der Vergleich mit Ollama. Ehrlich eingeordnet:

  • Für Docker-zentrierte Umgebungen ist die Integration angenehm — dasselbe Werkzeug, dieselbe Verwaltung, keine zusätzliche Installation.
  • Für Einzelplatznutzung und Experimente ist Ollama derzeit reifer: größere Modellauswahl, mehr Werkzeuge im Umfeld, mehr Erfahrungsberichte.
  • Für Mehrbenutzerbetrieb ist keines von beiden das richtige Werkzeug; dort gehört eine auf Durchsatz ausgelegte Inferenzlösung hin.

Wer den Vergleich selbst ziehen will, findet den Unterbau in Ollama: lokale Sprachmodelle betreiben und die Modellauswahl in Welches Ollama-Modell?.

Zwei Beispiele aus dem Alltag

Eine lokale KI-Umgebung mit Oberfläche:

services:
  ollama:
    image: ollama/ollama
    volumes:
      - ollama:/root/.ollama
    ports:
      - "127.0.0.1:11434:11434"
    restart: unless-stopped

  webui:
    image: ghcr.io/open-webui/open-webui:main
    depends_on:
      - ollama
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    volumes:
      - webui:/app/backend/data
    ports:
      - "127.0.0.1:3000:8080"
    restart: unless-stopped

volumes:
  ollama:
  webui:

Drei Details: Beide Ports sind an 127.0.0.1 gebunden, weil weder Ollama noch die Oberfläche standardmäßig abgesichert sind. Die Dienste sprechen intern über den Dienstnamen ollama, nicht über localhost. Und beide haben ein Volume — ohne das sind Modelle und Gesprächsverläufe nach jedem Neustart weg. Den vollständigen Aufbau beschreibe ich in Lokale ChatGPT-Alternative mit Open WebUI und Ollama.

Eine Vektordatenbank für Entwicklungszwecke:

services:
  qdrant:
    image: qdrant/qdrant
    ports:
      - "127.0.0.1:6333:6333"
    volumes:
      - qdrant:/qdrant/storage
    restart: unless-stopped

volumes:
  qdrant:

Welche Datenbank wofür passt — und ab wann überhaupt eine spezialisierte nötig ist —, steht im Vektordatenbanken-Vergleich.

Häufige Fragen

Was ist der Build Context? Das Verzeichnis, das beim Bauen vollständig an den Docker-Daemon übertragen wird, bevor die erste Anweisung im Dockerfile läuft.

Warum dauert mein Build so lange, bevor etwas passiert? Weil der gesamte Build Context übertragen wird. Die Ausgabe nennt die Größe — meist stecken Abhängigkeitsverzeichnisse und .git darin.

Was gehört in die .dockerignore? Abhängigkeitsverzeichnisse, .git, Build-Artefakte, Protokolle, Editor-Dateien und alles mit Zugangsdaten. Die Syntax unterscheidet sich von .gitignore.

Was ist der Unterschied zwischen docker context und Build Context? Der Build Context ist, was übertragen wird. docker context bestimmt, wohin — also mit welchem Daemon deine Befehle sprechen.

Wie setzt man den Build Context in Compose? Über build.context und, falls das Dockerfile woanders liegt, zusätzlich build.dockerfile. Im Monorepo braucht man die Wurzel als Context, wenn gemeinsamer Code eingebunden wird.

Verkleinern mehrstufige Builds den Context? Nein, nur das Endabbild. Für einen kleineren Context braucht es die .dockerignore.

Zusammenfassung und praktische Tipps

Zwei Begriffe, eine Verwechslung — und eine Datei, die die meisten Bauzeitprobleme löst. Die wichtigsten Punkte:

  • Der Build Context wird vollständig übertragen, bevor irgendetwas gebaut wird.
  • COPY ../ funktioniert nicht, weil außerhalb des Contexts nichts existiert.
  • .dockerignore ist die wirksamste einzelne Maßnahme für schnelle Builds — mit eigener Syntax, nicht der von .gitignore.
  • .env gehört ausgeschlossen. Was einmal in einer Schicht liegt, bleibt lesbar.
  • docker context ist etwas völlig anderes: der Ziel-Daemon, nicht der Inhalt.
  • Mehrstufige Builds verkleinern das Abbild, nicht den Context.
  • Abhängigkeitsdatei vor Quellcode kopieren — der wirksamste Griff für die Zwischenspeicherung.

Mein Rat für den Einstieg: Schau dir bei deinem nächsten Build die Zeile mit der übertragenen Größe an. Liegt sie über fünfzig Megabyte, schreib eine .dockerignore und miss danach erneut. Diese eine Datei kostet zehn Minuten und ist in den meisten Projekten der größte einzelne Zeitgewinn, den man an der Bauzeit erzielen kann.

Ressourcen