Docker-Compose-KI-/ML-Dev-Stack: Lokales LLM, Vector-DB, volle YAML
Jedes KI-Projekt startet bei mir jetzt gleich: docker compose up -d und ich habe Ollama, Qdrant, Postgres, Redis und einen LiteLLM-Proxy in unter zwei Minuten laufen. Keine pyenv-Konflikte, kein Homebrew-Drift, kein “läuft auf meinem Rechner”. Eine YAML, ein Kommando, identischer Stack auf Laptop und Dev-VPS.
Das ist das Tutorial für einen vollständigen Docker-Compose-KI-/ML-Dev-Stack. YAML kopieren, starten, Modell ziehen, bauen. Ich nutze genau dieses Layout für RAG-Pipeline-Prototyping, MCP-Server-Tests und meine Cron-gesteuerten Claude-Agents, bevor sie in Produktion gehen.
Der Stack passt auf ein 16-GB-Laptop. Er skaliert auch auf einen Hetzner CCX43 für eine geteilte Team-Dev-Umgebung. Was er nicht ist: ein Produktions-Setup. Für Self-Hosted-LLM-Inferenz at scale: Kubernetes-Leitfaden. Dieser hier ist für den Loop, in dem Sie wirklich iterieren.
Warum Docker Compose für KI-Dev
Ich habe drei Pfade ausprobiert, bevor ich bei Compose für lokale KI-Dev gelandet bin.
Bare-Metal-Install funktionierte, bis ich drei Python-Versionen hatte, die sich um torch stritten, und ein Ollama-Binary, das leise seinen Modell-Cache verschoben hatte. Jedes neue Projekt hat meine Dev-Box resettet. CUDA-Versionen driften. Pip-Installs kreuzkontaminieren. Als ich die Maschine neu aufgesetzt habe, verlor ich zwei Tage beim Rebuilden.
Kubernetes war das gegenteilige Problem. Minikube und k3d funktionieren, aber der Overhead — Manifests schreiben, Namespaces, Ingresses, Service Accounts und Port-Forwarding für ein Projekt, das ich vielleicht in einer Woche lösche — ist es nicht wert. Kubernetes gehört in Produktion, wo Operations-Kosten sich in Zuverlässigkeit und Autoscaling auszahlen. Nicht in den engen Inneren-Loop beim Ausprobieren eines neuen Modells oder Austauschen einer Vector-DB.
Docker Compose sitzt in der Mitte. Eine Datei beschreibt den Stack. docker compose up startet ihn. docker compose down stoppt ihn. docker compose down -v löscht jedes Volume und lässt mich sauber starten. Das Pattern ist die schnellste Feedback-Schleife, die ich gefunden habe, und dieselbe YAML zieht auf einen Staging-VPS, wenn ich sie mit jemandem teilen muss. Compose ist der kürzeste Weg zwischen “Idee” und “laufendem Code, der ein echtes LLM, eine echte Vector-DB und ein echtes Postgres berührt”.
Drei konkrete Gewinne:
- Reproduzierbar. Ein neuer Engineer clont das Repo, startet ein Kommando, der Stack ist identisch zu meinem. Onboarding geht von einem halben Tag Install-README-Debugging zu zehn Minuten Image-Pull.
- Isoliert. Nichts verschmutzt den Host. System-Python bleibt sauber. Node-Versionen kollidieren nicht. Wenn ich eine DB in Dev zerlege, zerlege ich ein Docker-Volume, nicht meine Workstation.
- Wegwerfbar. Teardown ist instant. Ich kann den ganzen Kram in dreißig Sekunden wegblasen und neu bauen. Zählt mehr, als es klingt. Die Kosten zum Experimentieren fallen gegen Null, also experimentiere ich mehr.
Was wir bauen
Ziel: lokale KI-Dev-Umgebung mit sieben Services. Fünf core, zwei optional — die ich aber fast immer drin habe.
Core-Services:
- Ollama. Lokaler LLM-Runner. Zieht GGUF-quantisierte Modelle und stellt eine OpenAI-kompatible API auf Port 11434 bereit. Handhabt Llama 3.3, Qwen 2.5, Mistral, Phi und alles andere aus der Ollama-Registry.
- Qdrant. Vector-DB. Ich nutze sie für RAG-Prototyping, Embedding-Search und semantische Task-Suche. Web-Dashboard auf Port 6333.
- Postgres 16. Genereller relationaler Store. Jede KI-App braucht irgendwann einen für Tasks, User, Job-Queues, Audit-Logs.
- Redis 7. Cache-Layer und Background-Queue. Paart mit BullMQ, Celery oder Sidekiq.
Optional, aber empfehlenswert:
- LiteLLM-Proxy. Einheitliche OpenAI-kompatible API vor Ollama, Claude und OpenAI. Provider via Config-Change tauschen, nicht Code-Change.
- Grafana + Prometheus. Observability. Prometheus scraped Metriken von LiteLLM und Postgres. Grafana-Dashboards zeigen Tokens/Sekunde, Cache-Hit-Rate, Queue-Depth.
- n8n. Visuelle Automatisierung zum Prototyping von Agent-Workflows, bevor ich sie als Bash oder Go umschreibe.
Deckt Ollama-Docker-Compose, Local-LLM-Docker, Vector-Search, Data-Persistence und Metriken ab. Alles, was ein Prototyp braucht.
Die vollständige docker-compose.yml
Als docker-compose.yml im Projekt-Root speichern. Das ist der komplette Stack.
services:
ollama:
image: ollama/ollama:latest
container_name: ai-ollama
ports:
- "11434:11434"
volumes:
- ollama:/root/.ollama
restart: unless-stopped
# Remove the deploy block if you have no NVIDIA GPU.
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
qdrant:
image: qdrant/qdrant:latest
container_name: ai-qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant:/qdrant/storage
restart: unless-stopped
postgres:
image: postgres:16-alpine
container_name: ai-postgres
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: ${POSTGRES_USER:-aidev}
POSTGRES_DB: ${POSTGRES_DB:-ai_dev}
ports:
- "5432:5432"
volumes:
- pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-aidev}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: ai-redis
ports:
- "6379:6379"
volumes:
- redis:/data
restart: unless-stopped
litellm:
image: ghcr.io/berriai/litellm:main-latest
container_name: ai-litellm
ports:
- "4000:4000"
environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY}
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
volumes:
- ./litellm.config.yaml:/app/config.yaml:ro
command: ["--config", "/app/config.yaml", "--port", "4000"]
depends_on:
- ollama
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
container_name: ai-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: ai-grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
volumes:
- grafana:/var/lib/grafana
depends_on:
- prometheus
restart: unless-stopped
n8n:
image: n8nio/n8n:latest
container_name: ai-n8n
ports:
- "5678:5678"
environment:
N8N_BASIC_AUTH_ACTIVE: "true"
N8N_BASIC_AUTH_USER: ${N8N_USER:-admin}
N8N_BASIC_AUTH_PASSWORD: ${N8N_PASSWORD}
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB:-ai_dev}
DB_POSTGRESDB_USER: ${POSTGRES_USER:-aidev}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- n8n:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
volumes:
ollama:
qdrant:
pg:
redis:
prometheus:
grafana:
n8n:
Und die .env-Datei daneben:
# .env
POSTGRES_USER=aidev
POSTGRES_PASSWORD=changeme_local_only
POSTGRES_DB=ai_dev
LITELLM_MASTER_KEY=sk-local-dev-key
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GRAFANA_PASSWORD=admin
N8N_USER=admin
N8N_PASSWORD=changeme_local_only
Minimale litellm.config.yaml:
model_list:
- model_name: local-llama
litellm_params:
model: ollama/llama3.2:3b
api_base: http://ollama:11434
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gpt-4
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
Minimale prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: litellm
static_configs:
- targets: ["litellm:4000"]
First-Run-Kommandos
Aus dem Projektordner:
docker compose up -d
docker compose ps
Sie sollten acht Container laufen sehen. Als Nächstes ein Modell in Ollama ziehen. Klein anfangen — ein 3B-Modell passt auf jedes moderne Laptop und ist schnell genug zum Iterieren:
docker exec -it ai-ollama ollama pull llama3.2:3b
Bei 24 GB+ VRAM ein größeres ziehen:
docker exec -it ai-ollama ollama pull llama3.3:70b-instruct-q4_K_M
Ollama-Antwort verifizieren:
curl http://localhost:11434/api/generate \
-d '{"model":"llama3.2:3b","prompt":"What is Docker Compose in one sentence?","stream":false}'
Qdrant checken:
curl http://localhost:6333/collections
LiteLLM-Proxy bestätigen:
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-local-dev-key" \
-H "Content-Type: application/json" \
-d '{"model":"local-llama","messages":[{"role":"user","content":"ping"}]}'
Grafana unter http://localhost:3000, Qdrant-Dashboard unter http://localhost:6333/dashboard, n8n unter http://localhost:5678.
GPU-Support-Notizen
Linux mit NVIDIA ist der Happy Path. NVIDIA Container Toolkit installieren:
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker
GPU-Sichtbarkeit im Container verifizieren:
docker exec -it ai-ollama nvidia-smi
Auf macOS hat Docker Desktop keinen GPU-Passthrough. Wer auf einem M-Series-Mac sitzt und sich um Inferenzgeschwindigkeit schert: Ollama nativ auf dem Host laufen lassen (brew install ollama), den Rest des Compose-Stacks auf http://host.docker.internal:11434 zeigen und den ollama-Service aus der YAML entfernen. Sie behalten die containerisierte Vector-DB und Daten-Services, ohne mit Metal zu kämpfen.
CPU-only ist für 3B- bis 7B-Modelle viabel. Erwarten Sie 5 bis 20 Tokens/Sekunde auf einem modernen Ryzen oder M-Series. Ok für Dev, schmerzhaft für interaktiv.
Dev-Services für Ihren App-Code hinzufügen
Compose ist nicht nur für Infrastruktur. Ihr eigener App-Code läuft hier auch. Einen Service hinzufügen, der Ihre Source mountet und Hot-Reloads macht.
TypeScript/Node-Beispiel:
app:
image: node:22-alpine
working_dir: /app
volumes:
- ./src:/app/src
- ./package.json:/app/package.json
- ./tsconfig.json:/app/tsconfig.json
- node_modules:/app/node_modules
command: sh -c "npm install && npm run dev"
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://aidev:${POSTGRES_PASSWORD}@postgres:5432/ai_dev
REDIS_URL: redis://redis:6379
OLLAMA_URL: http://ollama:11434
LITELLM_URL: http://litellm:4000
depends_on:
- postgres
- redis
- ollama
Python/FastAPI-Variante:
app:
image: python:3.12-slim
working_dir: /app
volumes:
- ./:/app
command: sh -c "pip install -r requirements.txt && uvicorn main:app --host 0.0.0.0 --reload"
ports:
- "8000:8000"
depends_on:
- postgres
- qdrant
- ollama
In Ihrer App den Ollama-Client auf http://ollama:11434 zeigen, nicht localhost. Docker Compose legt ein Netzwerk an, in dem Service-Namen als Hostnamen auflösen.
Übliche Dev-Patterns
Vier Patterns, die ich bei fast jedem Projekt nutze.
RAG-Prototyping mit Qdrant. Lokale Docs ingesten, mit einem kleinen Sentence-Transformer-Modell embedden (oder Ollamas nomic-embed-text), Vectors nach Qdrant schreiben. Mit Filter querien, Ergebnisse im Qdrant-Dashboard unter http://localhost:6333/dashboard inspizieren, dann die retrieveten Chunks via LiteLLM in einen Prompt verdrahten. Vollständiger Walkthrough: RAG-Pipeline-Tutorial. Für die richtige Vector-DB jenseits von Dev: Qdrant vs. Pinecone vs. Weaviate.
LiteLLM-Swap zwischen Local und Cloud. In Dev treffe ich local-llama durch LiteLLM. In Staging flippe ich den Modellnamen auf claude-sonnet. Mein App-Code ändert sich nicht. Eine Env-Variable, anderes Backend. Das einzelne nützlichste Teil im ganzen Stack. Bedeutet: dieselbe Codebase läuft offline im Flugzeug mit einem 3B-Modell — oder in einem bezahlten Pilot auf Claude Sonnet, ohne Code-Änderung.
Compose als MCP-Host. Ich fahre Claude Code auf dem Host und zeige ihn auf das containerisierte Postgres über den Postgres-MCP-Server. Kombiniert mit Filesystem-MCP und einem SQLite-MCP kann Claude Code während der Entwicklung echte Daten queryen, ohne Cloud-Round-Trip. Schema-Introspection, Row-Counts, Sample-Queries — alles aus dem Editor heraus. Kombiniert mit dem Ollama-Container kann Claude Code sogar auf ein lokales Modell zurückfallen, wenn ich rate-limited bin.
Linux-VPS gleich hosten. Ich wiederverwende dieses Compose-File auf dem VPS für Team-Dev. Gleiche Services, gleiche Ports, andere .env. Der Linux-VPS-KI-Dev-Setup-Leitfaden deckt Firewall, Reverse Proxy und TLS obendrauf. Die Kerninsight: Dev und Staging teilen Infrastruktur-Code, sodass nichts überrascht, wenn ich dasselbe Compose auf eine größere Box schiebe.
Stack erweitern
Was ich projektabhängig ergänze:
- JupyterLab für Notebook-Exploration:
jupyter/scipy-notebook,./notebooksmounten, Port 8888. - MinIO für S3-kompatibles Object-Storage beim offlinen Testen von Datei-Pipelines:
minio/minio, Ports 9000 und 9001. - pgvector-Extension in Postgres, wenn Embeddings neben relationalen Daten liegen sollen. Image auf
pgvector/pgvector:pg16tauschen. - Traefik als Reverse Proxy, sobald der Stack mehr als drei HTTP-Services hat. Gibt jedem Service einen netten
*.localhost-Hostnamen. - n8n für Agent-Workflow-Prototyping. Ich skizziere Agent-Flows in n8n, bevor ich sie als Code umschreibe. Für produktives Self-Hosting: n8n-Self-Hosting-Leitfaden.
Cleanup
Alles stoppen, Daten behalten:
docker compose down
Alles stoppen und Volumes löschen:
docker compose down -v
Speicher von ungenutzten Images und Build-Cache zurückholen:
docker system prune -a
Ollama-Model-Blobs sind die größten Disk-Fresser. Nutzung checken:
docker system df -v | grep ollama
Wechsel in die Produktion
Der Stack ist ein Dev-Stack. Nicht as-is deployen.
Für produktive Self-Hosted-LLM-Inferenz: Self-Hosted-LLM-Kubernetes-Leitfaden. Deckt GPU-Scheduling, Model-Sharding und ordentliches Autoscaling für Inferenz-Pods ab.
Für eine Mitteloption läuft dasselbe Compose auf einem VPS für Staging. systemd für docker compose up als Service, Traefik davor für TLS, Postgres-Backups nach Zeitplan. Gut genug für interne Tools und bezahlte Piloten. Nicht gut genug für User-facing Workloads.
Troubleshooting
“Ollama: GPU not detected”. docker exec -it ai-ollama nvidia-smi. Scheitert: NVIDIA Container Toolkit ist nicht installiert, oder der Docker-Daemon wurde nach Install nicht neu gestartet. Auf WSL2 prüfen, dass der Host-Treiber die WSL-kompatible Build ist.
“Out of memory on Ollama”. Auf ein quantisiertes Modell wechseln. llama3.3:70b-instruct-q4_K_M nutzt rund 40 GB VRAM. Für 8 bis 16 GB Karten llama3.2:8b-instruct-q4_K_M oder die 3B-Variante. Ollama splittet automatisch zwischen VRAM und CPU-RAM, also reicht oft ein Partial-Offload.
“Qdrant will not start”. Port-6333-Konflikt. Anderer Prozess nutzt ihn. sudo lsof -i :6333. Oder Compose-Port-Mapping auf 16333:6333 ändern.
“Postgres-Daten weg nach Restart”. Der volumes:-Block unten fehlt, oder der Service referenziert ihn nicht. Ohne Named Volumes nutzt Docker Anonymous Volumes, die garbage-collected werden.
“Slow inference on CPU”. Sie fahren ein Modell, das zu groß für Ihre Hardware ist. Auf 3B runter oder via LiteLLM eine API für den Slow Path nehmen. Ollama für Offline-Iteration behalten und Claude Sonnet für alles Interaktive treffen.
“n8n kann Postgres nicht erreichen”. Das depends_on mit condition: service_healthy erfordert den Healthcheck auf Postgres. Falls gelöscht, racet n8n die DB beim Start und crasht. Reinlegen oder Retry-Loop in der n8n-Config.
“LiteLLM liefert 401”. LITELLM_MASTER_KEY muss im Authorization: Bearer-Header auf jedem Request übergeben werden. .env-Ladung prüfen (docker compose config zeigt die aufgelösten Werte).
“Ollama pull hängt bei großem Modell”. Ollama lädt in Chunks über HTTPS. Corporate- oder Hotel-Netzwerke killen gelegentlich Long-Lived-Connections. Pull auf stabiler Verbindung, oder außerhalb des Containers mit curl gegen die Ollama-Registry und Blobs ins Volume mounten.
“LiteLLM erreicht Ollama nicht”. Innerhalb des Compose-Netzwerks http://ollama:11434, nicht localhost. localhost im LiteLLM-Container ist der Container selbst.
“docker compose up ist beim ersten Lauf langsam”. Erster Lauf pulled sieben oder acht Images. Bei langsamer Verbindung sind das 3 bis 5 GB Layer. Nachfolgende up-Calls sind instant. docker compose pull in einem Warm-up-Schritt, wenn Sie den Stack oft zyklen.
“Qdrant-Dashboard zeigt keine Collections”. Sie haben noch keine angelegt. Qdrant startet leer. Eine anlegen: curl -X PUT http://localhost:6333/collections/my_docs -H 'Content-Type: application/json' -d '{"vectors":{"size":768,"distance":"Cosine"}}'.