Tipps & Tricks

Docker für Python- und Datenprojekte: Ein umfassender Einstieg

11 min Lesezeit
Docker für Python- und Datenprojekte: Ein umfassender Einstieg

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:

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

Mehr zum Thema