Die Verwaltung von Abhängigkeiten in Python-Datenprojekten kann schnell unübersichtlich werden. Docker bietet eine Lösung, indem es konsistente Umgebungen schafft, die einfach erstellt, geteilt und bereitgestellt werden können.
Einleitung
Python und Datenprojekte haben ein Problem mit Abhängigkeiten. Zwischen verschiedenen Python-Versionen, virtuellen Umgebungen, systemweiten Paketen und Unterschieden in den Betriebssystemen kann es manchmal länger dauern, den Code eines anderen zum Laufen zu bringen, als den Code selbst zu verstehen.
Docker löst dieses Problem, indem es Ihren Code und seine gesamte Umgebung – einschließlich der Python-Version, Abhängigkeiten und Systembibliotheken – in einem einzigen Artefakt, dem sogenannten Image, verpackt. Aus diesem Image können Sie Container starten, die identisch auf Ihrem Laptop, dem Rechner Ihres Teamkollegen und einem Cloud-Server laufen. Sie hören auf, Umgebungen zu debuggen, und beginnen, Ihre Arbeit zu versenden.
In diesem Artikel lernen Sie Docker anhand praktischer Beispiele mit einem Fokus auf Datenprojekte kennen: das Containerisieren eines Skripts, das Bereitstellen eines Machine-Learning-Modells mit FastAPI, das Verknüpfen einer Multi-Service-Pipeline mit Docker Compose und das Planen eines Jobs mit einem Cron-Container.
Voraussetzungen
Bevor Sie mit den Beispielen beginnen, benötigen Sie:
- Docker und Docker Compose, die für Ihr Betriebssystem installiert sind. Folgen Sie der offiziellen Installationsanleitung für Ihre Plattform.
- Grundkenntnisse der Kommandozeile und von Python.
- Kenntnisse im Schreiben einer Dockerfile, dem Erstellen eines Images und dem Ausführen eines Containers aus diesem Image.
Wenn Sie eine kurze Auffrischung benötigen, finden Sie hier einige Artikel, die Ihnen helfen:
- 10 wesentliche Docker-Konzepte in weniger als 10 Minuten erklärt
- Eine sanfte Einführung in Docker für Python-Entwickler
- Effiziente Python-Skripte zur Automatisierung der explorativen Datenanalyse
Sie benötigen kein tiefes Docker-Wissen, um folgen zu können. Jedes Beispiel erklärt, was passiert.
Containerisieren eines Python-Skripts mit festgelegten Abhängigkeiten
Beginnen wir mit dem häufigsten Anwendungsfall: Sie haben ein Python-Skript und eine requirements.txt-Datei, und Sie möchten, dass es überall zuverlässig läuft.
Wir werden ein Datenbereinigungsskript erstellen, das eine rohe Verkaufs-CSV-Datei liest, Duplikate entfernt, fehlende Werte auffüllt und eine bereinigte Version auf der Festplatte speichert.
Projektstruktur
Das Projekt ist wie folgt organisiert:
data-cleaner/
├── Dockerfile
├── requirements.txt
├── clean_data.py
└── data/
└── raw_sales.csv
Das Skript schreiben
Hier ist das Datenbereinigungsskript, das Pandas für die Hauptarbeit verwendet:
# clean_data.py
import pandas as pd
import os
INPUT_PATH = "data/raw_sales.csv"
OUTPUT_PATH = "data/cleaned_sales.csv"
print("Reading data...")
df = pd.read_csv(INPUT_PATH)
print(f"Rows before cleaning: {len(df)}")
# Duplikate entfernen
df = df.drop_duplicates()
# Fehlende numerische Werte mit dem Median der Spalte auffüllen
for col in df.select_dtypes(include='number').columns:
df[col] = df[col].fillna(df[col].median())
# Fehlende Textwerte mit 'Unbekannt' auffüllen
for col in df.select_dtypes(include='object').columns:
df[col] = df[col].fillna('Unknown')
print(f"Rows after cleaning: {len(df)}")
df.to_csv(OUTPUT_PATH, index=False)
print(f"Cleaned file saved to {OUTPUT_PATH}")
Abhängigkeiten festlegen
Das Festlegen exakter Versionen ist wichtig. Ohne dies könnte pip install pandas unterschiedliche Versionen auf verschiedenen Maschinen installieren. Festgelegte Versionen garantieren, dass jeder dasselbe Verhalten erhält. Sie können die exakten Versionen in der requirements.txt-Datei wie folgt definieren:
pandas==2.2.0
openpyxl==3.1.2
Die Dockerfile definieren
Diese Dockerfile erstellt ein minimales, cachefreundliches Image für das Bereinigungsskript:
# Verwenden Sie ein schlankes Python 3.11-Basisimage
FROM python:3.11-slim
# Setzen Sie das Arbeitsverzeichnis im Container
WORKDIR /app
# Kopieren und installieren Sie zuerst die Abhängigkeiten (für die Layer-Caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Kopieren Sie das Skript in den Container
COPY clean_data.py .
# Standardbefehl, der beim Start des Containers ausgeführt wird
CMD ["python", "clean_data.py"]
Es gibt einige Punkte, die hier erwähnenswert sind. Wir verwenden python:3.11-slim anstelle des vollständigen Python-Images, da es erheblich kleiner ist und Pakete entfernt, die Sie nicht benötigen.
Wir kopieren requirements.txt, bevor wir den Rest des Codes kopieren, und das ist absichtlich. Docker baut Images in Schichten und cached jede davon. Wenn Sie nur clean_data.py ändern, wird Docker nicht alle Ihre Abhängigkeiten beim nächsten Build neu installieren. Es verwendet die zwischengespeicherte pip-Schicht und springt direkt zum Kopieren Ihres aktualisierten Skripts. Diese kleine Anordnung kann Ihnen Minuten an Wiederaufbauzeit sparen.
Erstellen und Ausführen
Nachdem das Image erstellt wurde, können Sie den Container ausführen und Ihren lokalen Datenordner einbinden:
# Erstellen Sie das Image und taggen Sie es
docker build -t data-cleaner .
# Führen Sie es aus und binden Sie Ihren lokalen data/-Ordner in den Container ein
docker run --rm -v $(pwd)/data:/app/data data-cleaner
Das -v $(pwd)/data:/app/data-Flag bindet Ihren lokalen data/-Ordner in den Container unter /app/data ein. So liest das Skript Ihre CSV und so wird die bereinigte Ausgabe zurück auf Ihre Maschine geschrieben. Nichts ist im Image eingebettet, und die Daten bleiben auf Ihrem Dateisystem.
Das –rm-Flag entfernt den Container automatisch, nachdem er fertig ist. Da dies ein einmaliges Skript ist, gibt es keinen Grund, einen gestoppten Container herumliegen zu lassen.
Ein Machine-Learning-Modell mit FastAPI bereitstellen
Sie haben ein Modell trainiert und möchten es über HTTP verfügbar machen, damit andere Dienste Daten senden und Vorhersagen zurückerhalten können. FastAPI eignet sich hervorragend dafür: Es ist schnell, leichtgewichtig und übernimmt die Eingangsvalidierung mit Pydantic.
Projektstruktur
Das Projekt trennt das Modellartefakt vom Anwendungscode:
ml-api/
├── Dockerfile
├── requirements.txt
├── app.py
└── model.pkl
Die App schreiben
Die folgende App lädt das Modell einmal beim Start und stellt einen /predict-Endpunkt zur Verfügung:
# app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import pickle
import numpy as np
app = FastAPI(title="Sales Forecast API")
# Modell einmal beim Start laden
with open("model.pkl", "rb") as f:
model = pickle.load(f)
class PredictRequest(BaseModel):
region: str
month: int
marketing_spend: float
units_in_stock: int
class PredictResponse(BaseModel):
region: str
predicted_revenue: float
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/predict", response_model=PredictResponse)
def predict(request: PredictRequest):
try:
features = [[
request.month,
request.marketing_spend,
request.units_in_stock
]]
prediction = model.predict(features)
return PredictResponse(
region=request.region,
predicted_revenue=round(float(prediction[0]), 2)
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Die Klasse PredictRequest übernimmt die Eingangsvalidierung für Sie. Wenn jemand eine Anfrage mit einem fehlenden Feld oder einem String anstelle einer Zahl sendet, lehnt FastAPI sie mit einer klaren Fehlermeldung ab, bevor Ihr Modellcode überhaupt ausgeführt wird. Das Modell wird einmal beim Start geladen – nicht bei jeder Anfrage – was die Antwortzeiten schnell hält.
Der /health-Endpunkt ist eine kleine, aber wichtige Ergänzung: Docker, Load-Balancer und Cloud-Plattformen verwenden ihn, um zu überprüfen, ob Ihr Dienst tatsächlich aktiv und bereit ist.
Die Dockerfile definieren
Diese Dockerfile integriert das Modell direkt in das Image, sodass der Container vollständig eigenständig ist:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Modell und App zusammen kopieren
COPY model.pkl .
COPY app.py .
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Das model.pkl wird zur Build-Zeit in das Image integriert. Das bedeutet, dass der Container vollständig eigenständig ist und Sie beim Ausführen nichts einbinden müssen. Das –host 0.0.0.0-Flag weist Uvicorn an, auf allen Netzwerk-Schnittstellen innerhalb des Containers zu lauschen, nicht nur auf localhost. Ohne dies können Sie die API von außerhalb des Containers nicht erreichen.
Erstellen und Ausführen
Erstellen Sie das Image und starten Sie den API-Server:
docker build -t ml-api .
docker run --rm -p 8000:8000 ml-api
Testen Sie es mit curl:
curl -X POST http://localhost:8000/predict \\
-H "Content-Type: application/json" \\
-d '{"region": "North", "month": 3, "marketing_spend": 5000.0, "units_in_stock": 320}'
Eine Multi-Service-Pipeline mit Docker Compose erstellen
In echten Datenprojekten sind selten nur ein Prozess beteiligt. Möglicherweise benötigen Sie eine Datenbank, ein Skript, das Daten in diese lädt, und ein Dashboard, das daraus liest – alles läuft zusammen.
Docker Compose ermöglicht es Ihnen, mehrere Container als eine einzige Anwendung zu definieren und auszuführen. Jeder Dienst hat seinen eigenen Container, aber sie teilen sich ein privates Netzwerk, sodass sie miteinander kommunizieren können.
Projektstruktur
Die Pipeline teilt jeden Dienst in sein eigenes Unterverzeichnis auf:
pipeline/
├── docker-compose.yml
├── loader/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── load_data.py
└── dashboard/
├── Dockerfile
├── requirements.txt
└── app.py
Die Compose-Datei definieren
Diese Compose-Datei erklärt alle drei Dienste und verbindet sie mit Gesundheitsprüfungen und gemeinsamen URL-Umgebungsvariablen:
# docker-compose.yml
version: "3.9"
services:
db:
image: postgres:15
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: analytics
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d analytics"]
interval: 5s
retries: 5
loader:
build: ./loader
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://admin:secret@db:5432/analytics
dashboard:
build: ./dashboard
depends_on:
db:
condition: service_healthy
ports:
- "8501:8501"
environment:
DATABASE_URL: postgresql://admin:secret@db:5432/analytics
volumes:
pgdata:
Das Loader-Skript schreiben
Dieses Skript wartet kurz auf die Datenbank und lädt dann eine CSV in die Verkaufstabelle mit SQLAlchemy:
# loader/load_data.py
import pandas as pd
from sqlalchemy import create_engine
import os
import time
DATABASE_URL = os.environ["DATABASE_URL"]
# Geben Sie der DB einen Moment, um vollständig bereit zu sein
time.sleep(3)
engine = create_engine(DATABASE_URL)
df = pd.read_csv("sales_data.csv")
df.to_sql("sales", engine, if_exists="replace", index=False)
print(f"Loaded {len(df)} rows into the sales table.")
Werfen wir einen genaueren Blick auf die Compose-Datei. Jeder Dienst läuft in seinem eigenen Container, aber sie befinden sich alle im selben von Docker verwalteten Netzwerk, sodass sie sich gegenseitig erreichen können, indem sie den Dienstnamen als Hostnamen verwenden. Der Loader verbindet sich mit db:5432 – und nicht mit localhost – weil db der Dienstname ist und Docker die DNS-Auflösung automatisch übernimmt.
Die Gesundheitsprüfung des PostgreSQL-Dienstes ist wichtig. depends_on wartet allein nur darauf, dass der Container startet, nicht darauf, dass PostgreSQL bereit ist, Verbindungen zu akzeptieren. Die Gesundheitsprüfung verwendet pg_isready, um zu bestätigen, dass die Datenbank tatsächlich aktiv ist, bevor der Loader versucht, sich zu verbinden. Das pgdata-Volume speichert die Datenbank zwischen den Ausführungen; das Stoppen und Neustarten der Pipeline löscht Ihre Daten nicht.
Alles starten
Starten Sie alle Dienste mit einem einzigen Befehl:
docker compose up --build
Um alles zu stoppen, führen Sie aus:
docker compose down
Jobs mit einem Cron-Container planen
Manchmal müssen Sie ein Skript nach einem Zeitplan ausführen. Vielleicht ruft es stündlich Daten von einer API ab und schreibt sie in eine Datenbank oder eine Datei. Sie möchten kein vollständiges Orchestrierungssystem wie Airflow für etwas so Einfaches einrichten. Ein Cron-Container erledigt die Aufgabe sauber.
Projektstruktur
Das Projekt umfasst eine Crontab-Datei neben dem Skript und der Dockerfile:
data-fetcher/
├── Dockerfile
├── requirements.txt
├── fetch_data.py
└── crontab
Das Fetch-Skript schreiben
Dieses Skript verwendet Requests, um einen API-Endpunkt zu erreichen und die Ergebnisse als zeitgestempelte CSV zu speichern:
# fetch_data.py
import requests
import pandas as pd
from datetime import datetime
import os
API_URL = "https://api.example.com/sales/latest"
OUTPUT_DIR = "/app/output"
os.makedirs(OUTPUT_DIR, exist_ok=True)
print(f"[{datetime.now()}] Fetching data...")
response = requests.get(API_URL, timeout=10)
response.raise_for_status()
data = response.json()
df = pd.DataFrame(data["records"])
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
output_path = f"{OUTPUT_DIR}/sales_{timestamp}.csv"
df.to_csv(output_path, index=False)
print(f"[{datetime.now()}] Saved {len(df)} records to {output_path}")
Die Crontab definieren
Die Crontab plant das Skript, um jede Stunde zu laufen, und leitet alle Ausgaben in eine Protokolldatei um:
# Jede Stunde zur vollen Stunde ausführen
0 * * * * python /app/fetch_data.py >> /var/log/fetch.log 2>&1
Der >> /var/log/fetch.log 2>&1-Teil leitet sowohl die Standardausgabe als auch die Fehlermeldung in eine Protokolldatei um. So können Sie nachträglich überprüfen, was passiert ist.
Die Dockerfile definieren
Diese Dockerfile installiert Cron, registriert den Zeitplan und hält ihn im Vordergrund aktiv:
FROM python:3.11-slim
# Cron