% ============================================
% STEP 03: DOCKER, DEPLOYMENT & ERSTE LIVE-APP
% ============================================

\section{Docker-Images bauen und App deployen}
\label{sec:step03}

In diesem Schritt bringen wir unsere Fitness-App vom lokalen Entwicklungsrechner auf den Server und machen sie weltweit erreichbar. Dafür nutzen wir \textbf{Docker} – eine Container-Plattform, die Anwendungen in standardisierten, isolierten Umgebungen verpackt und ausführt.

\subsection{Was ist Docker und warum nutzen wir es?}

Docker funktioniert wie ein \textbf{Versandkarton für Software}. Stell dir vor, du verschickst ein zerbrechliches Paket: Du packst es in einen genormten Karton, der überall auf der Welt gleich behandelt wird – egal ob in Deutschland, Japan oder Brasilien. Docker macht dasselbe mit Software:

\begin{itemize}
    \item \textbf{Image} = Der Bauplan des Kartons (inkl. Inhalt). Ein Image enthält das Betriebssystem, alle Abhängigkeiten und die Anwendung selbst.
    \item \textbf{Container} = Der tatsächliche laufende Karton. Ein Container ist eine laufende Instanz eines Images.
    \item \textbf{Volume} = Ein separater Speicherort, der den Container überlebt. Wie ein externer USB-Stick, den man an den Karton anschließt.
\end{itemize}

\textbf{Konkret für unser Projekt:}
\begin{itemize}
    \item \texttt{fitness-api:latest} – Image mit .NET 8 Backend
    \item \texttt{fitness-web:latest} – Image mit React Frontend + Nginx
    \item \texttt{fitness-data} – Volume für die SQLite-Datenbank (überlebt Container-Neustarts)
\end{itemize}

\subsection{Die drei Dockerfiles im Detail}

\subsubsection{Backend-Dockerfile: \texttt{apps/api/Dockerfile}}

\begin{lstlisting}[language=Dockerfile, caption={Dockerfile für das .NET Backend}]
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["apps/api/Api.csproj", "apps/api/"]
RUN dotnet restore "apps/api/Api.csproj"
COPY . .
WORKDIR /src/apps/api
RUN dotnet publish "Api.csproj" -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 5000
ENV ASPNETCORE_URLS=http://+:5000
ENTRYPOINT ["dotnet", "Api.dll"]
\end{lstlisting}

\textbf{Zeile für Zeile erklärt:}
\begin{enumerate}
    \item \texttt{FROM ... AS build} – Startet mit dem .NET 8 SDK Image (enthält Compiler, Tools). Der Alias \texttt{build} erlaubt später darauf zuzugreifen.
    \item \texttt{WORKDIR /src} – Setzt das Arbeitsverzeichnis im Container auf \texttt{/src}.
    \item \texttt{COPY ["apps/api/Api.csproj", "apps/api/"]} – Kopiert NUR die Projektdatei. Dadurch cached Docker diesen Schritt: Solange sich \texttt{Api.csproj} nicht ändert, wird der Cache verwendet $\rightarrow$ schnellere Builds!
    \item \texttt{RUN dotnet restore} – Lädt alle NuGet-Pakete herunter (Entity Framework, NSwag, Swagger usw.).
    \item \texttt{COPY . .} – Kopiert den gesamten restlichen Quellcode.
    \item \texttt{WORKDIR /src/apps/api} – Wechselt ins Backend-Verzeichnis.
    \item \texttt{RUN dotnet publish} – Kompiliert die Anwendung im Release-Modus in den Ordner \texttt{/app/publish}.
    \item \texttt{FROM ... AS final} – Startet ein NEUES, schlankeres Image (nur ASP.NET Runtime, kein SDK). Das spart Speicher!
    \item \texttt{COPY --from=build ...} – Kopiert die kompilierte Anwendung aus dem Build-Image.
    \item \texttt{EXPOSE 5000} – Dokumentiert, dass der Container auf Port 5000 lauscht.
    \item \texttt{ENV ASPNETCORE\_URLS=http://+:5000} – Sagt .NET, es soll auf Port 5000 auf ALLEN Netzwerkschnittstellen lauschen.
    \item \texttt{ENTRYPOINT ["dotnet", "Api.dll"]} – Startet die Anwendung beim Container-Start.
\end{enumerate}

\subsubsection{Frontend-Dockerfile: \texttt{apps/web/Dockerfile}}

\begin{lstlisting}[language=Dockerfile, caption={Dockerfile für das React Frontend}]
FROM node:22-alpine AS build
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/web/package.json apps/web/
RUN npm install -g pnpm && pnpm install --no-frozen-lockfile
COPY apps/web/ apps/web/
WORKDIR /app/apps/web
RUN pnpm run build

FROM nginx:stable-alpine
COPY --from=build /app/apps/web/dist /usr/share/nginx/html
COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
\end{lstlisting}

\textbf{Zeile für Zeile erklärt:}
\begin{enumerate}
    \item \texttt{FROM node:22-alpine} – Leichtgewichtiges Node.js 22 Image (Alpine Linux = nur $\sim$5 MB statt $\sim$180 MB bei Ubuntu).
    \item \texttt{COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./} – Kopiert die Monorepo-Konfiguration aus dem Root. \texttt{pnpm-workspace.yaml} ist nötig, damit pnpm die Workspace-Struktur erkennt.
    \item \texttt{COPY apps/web/package.json apps/web/} – Kopiert die Frontend-Paketliste an ihren Platz.
    \item \texttt{RUN npm install -g pnpm \&\& pnpm install} – Installiert pnpm global und dann alle Abhängigkeiten.
    \item \texttt{COPY apps/web/ apps/web/} – Kopiert den restlichen Frontend-Code.
    \item \texttt{WORKDIR /app/apps/web} – Wechselt ins Frontend-Verzeichnis.
    \item \texttt{RUN pnpm run build} – Baut das Frontend mit Vite (erzeugt \texttt{dist/}).
    \item \texttt{FROM nginx:stable-alpine} – NEUES schlankes Image mit Nginx (Webserver).
    \item \texttt{COPY --from=build ... /usr/share/nginx/html} – Kopiert den Build-Output in Nginx's Standard-Webverzeichnis.
    \item \texttt{COPY apps/web/nginx.conf ...} – Unsere eigene Nginx-Konfiguration.
    \item \texttt{EXPOSE 80} – HTTP-Port.
    \item \texttt{CMD ["nginx", "-g", "daemon off;"]} – Startet Nginx im Vordergrund (Container bleibt am Leben).
\end{enumerate}

\subsubsection{Nginx-Konfiguration: \texttt{apps/web/nginx.conf}}

\begin{lstlisting}[language=Bash, caption={Nginx-Konfiguration mit Reverse Proxy}]
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    location /api/ {
        proxy_pass http://fitness-api:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
\end{lstlisting}

\textbf{Erklärung:} Dies ist ein \textbf{Reverse Proxy}. Nginx nimmt alle Anfragen entgegen und entscheidet, wohin sie weitergeleitet werden:
\begin{itemize}
    \item \texttt{location /} – Anfragen an die Hauptseite $\rightarrow$ liefert React-Dateien aus \texttt{/usr/share/nginx/html}
    \item \texttt{location /api/} – Anfragen an \texttt{/api/*} $\rightarrow$ leitet sie an das Backend (\texttt{fitness-api:5000}) weiter
    \item \texttt{try\_files \$uri \$uri/ /index.html} – Sorgt dafür, dass Reacts Client-Side-Routing funktioniert (z.B. \texttt{/workouts/123} wird an React weitergegeben, nicht als 404 beantwortet)
\end{itemize}

\subsection{Das Backend: Program.cs im Detail}

\begin{lstlisting}[language=CSharp, caption={Vollständige Program.cs}]
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var dbPath = Path.Combine("/app/data", "fitness.db");
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite($"Data Source={dbPath}"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument();
builder.Services.AddSwaggerGen();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    db.Database.EnsureCreated();
}

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/api/workouts", async (Workout workout, AppDbContext db) => { ... });
app.MapGet("/api/workouts", async (AppDbContext db) => ...);
app.MapGet("/api/workouts/{id:guid}", async (Guid id, AppDbContext db) => ...);
app.MapPut("/api/workouts/{id:guid}", async (Guid id, Workout input, AppDbContext db) => ...);
app.MapDelete("/api/workouts/{id:guid}", async (Guid id, AppDbContext db) => ...);

app.MapGet("/", () => "H Fitness API läuft!");
app.Run();
\end{lstlisting}

\textbf{Die NuGet-Pakete in der .csproj-Datei:}
\begin{itemize}
    \item \texttt{Microsoft.EntityFrameworkCore.Sqlite} (8.0.14) – EF Core für SQLite
    \item \texttt{Microsoft.EntityFrameworkCore.Design} (8.0.14) – EF Core Tools für Migrationen
    \item \texttt{NSwag.AspNetCore} (14.1.0) – OpenAPI/Swagger-Generator
    \item \texttt{Swashbuckle.AspNetCore} (6.6.2) – Swagger UI (die schöne Oberfläche)
\end{itemize}

\textbf{Wichtige Details:}
\begin{itemize}
    \item \texttt{Path.Combine("/app/data", "fitness.db")} – Im Docker-Container liegt die Datenbank im Volume-Ordner \texttt{/app/data}. Das stellt sicher, dass die Daten erhalten bleiben, auch wenn der Container gelöscht und neu erstellt wird.
    \item \texttt{db.Database.EnsureCreated()} – Erstellt die Datenbank-Tabellen automatisch beim Start, falls sie noch nicht existieren. Erspart uns manuelle Migrationen im Produktivbetrieb.
    \item Die CRUD-Endpunkte sind \textbf{Minimal API Endpoints} – .NET 8's leichtgewichtige Alternative zu Controllern.
\end{itemize}

\subsection{Der API-Client: client.ts im Detail}

\begin{lstlisting}[language=TypeScript, caption={Manueller API-Client}]
const API_BASE = "";

export interface Workout {
  id?: string;
  name: string;
  date: string;
  durationMinutes: number;
  notes?: string;
}

export const fitnessApi = {
  async getWorkouts(): Promise<Workout[]> {
    const res = await fetch(`/api/workouts`);
    return res.json();
  },

  async createWorkout(workout: Workout): Promise<Workout> {
    const res = await fetch(`/api/workouts`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(workout),
    });
    return res.json();
  },

  // Weitere Methoden: getWorkout, updateWorkout, deleteWorkout...
};
\end{lstlisting}

\textbf{Erklärung:}
\begin{itemize}
    \item \texttt{API\_BASE = ""} – Keine absolute URL! Stattdessen relative Pfade wie \texttt{/api/workouts}. Der Browser sendet die Anfrage dann an dieselbe Domain, auf der die Seite gehostet ist. Nginx leitet sie an das Backend weiter.
    \item \texttt{interface Workout} – TypeScript-Interface für Typsicherheit. Stellt sicher, dass wir keine falschen Felder an die API senden.
    \item \texttt{fetch()} – Native Browser-API für HTTP-Anfragen. Kein Axios, kein jQuery nötig!
    \item Die Methoden geben direkt das geparste JSON zurück.
\end{itemize}

\subsection{Das Frontend: App.tsx im Detail}

\begin{lstlisting}[language=TypeScript, caption={Hauptkomponente mit Workout-Liste und Formular}]
function App() {
  const [workouts, setWorkouts] = useState<Workout[]>([]);
  const [form, setForm] = useState<Workout>({ ... });

  const loadWorkouts = async () => {
    const data = await fitnessApi.getWorkouts();
    setWorkouts(data);
  };

  useEffect(() => { loadWorkouts(); }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await fitnessApi.createWorkout({ ...form });
    loadWorkouts();
  };

  return (
    <div className="min-h-screen bg-gray-950 text-white p-4 max-w-md mx-auto">
      {/* Formular */}
      <form onSubmit={handleSubmit}>...</form>
      {/* Workout-Liste */}
      {workouts.map(w => ( ... ))}
    </div>
  );
}
\end{lstlisting}

\textbf{Erklärung:}
\begin{itemize}
    \item \texttt{useState} – Reacts State-Management für die Workout-Liste und das Formular.
    \item \texttt{useEffect} – Lädt die Workouts einmalig beim ersten Rendern der Komponente.
    \item \texttt{handleSubmit} – Wird beim Absenden des Formulars aufgerufen. Verhindert den Standard-Seiten-Reload (\texttt{e.preventDefault()}), sendet das Workout an die API und lädt die Liste neu.
    \item Tailwind CSS-Klassen wie \texttt{bg-gray-950}, \texttt{text-white}, \texttt{p-4} gestalten die App im Dark-Mode.
\end{itemize}

\subsection{Images bauen}

Am Anfang war das Frontend-Image fehlerhaft. Hier die drei wichtigsten Fixes:

\textbf{Fehler 1: \texttt{pnpm-lock.yaml} nicht gefunden}
\begin{itemize}
    \item Ursache: Dockerfile suchte in \texttt{apps/web/}, aber die Datei liegt im Root.
    \item Lösung: \texttt{COPY pnpm-lock.yaml ./} (vom Root kopieren).
\end{itemize}

\textbf{Fehler 2: \texttt{--frozen-lockfile} schlug fehl}
\begin{itemize}
    \item Ursache: pnpm-Lockfile war nicht aktuell mit der Root-\texttt{package.json}.
    \item Lösung: \texttt{--no-frozen-lockfile} verwenden, damit pnpm fehlende Pakete nachinstalliert.
\end{itemize}

\textbf{Fehler 3: TypeScript Compiler (\texttt{tsc}) nicht gefunden}
\begin{itemize}
    \item Ursache: \texttt{tsc -b} benötigt TypeScript als Abhängigkeit, die im Container fehlte.
    \item Lösung: Build-Script von \texttt{"tsc -b \&\& vite build"} auf \texttt{"vite build"} geändert. Vite führt den TypeScript-Check beim Dev-Server durch – im Produktions-Build reicht die reine Vite-Kompilierung.
\end{itemize}

\subsection{Images exportieren und auf den Server kopieren}

\begin{lstlisting}[language=Bash, caption={Images exportieren und kopieren}]
# Images als tar-Datei speichern
docker save fitness-api:latest fitness-web:latest -o fitness-images.tar

# Auf den Server kopieren (scp = Secure Copy über SSH)
scp fitness-images.tar testserver:/root/

# Auf dem Server importieren
ssh testserver
docker load -i /root/fitness-images.tar
docker images | grep fitness
\end{lstlisting}

\textbf{Befehle erklärt:}
\begin{itemize}
    \item \texttt{docker save} – Exportiert Docker-Images in eine portable tar-Datei.
    \item \texttt{scp} – Secure Copy: Kopiert Dateien verschlüsselt über SSH.
    \item \texttt{testserver:/root/} – Der Alias aus unserer \texttt{\textasciitilde/.ssh/config}. Die Datei landet im \texttt{/root/}-Verzeichnis des Servers.
    \item \texttt{docker load -i} – Importiert Images aus einer tar-Datei in Docker.
    \item \texttt{docker images} – Listet alle lokal verfügbaren Docker-Images auf.
\end{itemize}

\subsection{Container auf dem Server starten}

\begin{lstlisting}[language=Bash, caption={Docker-Netzwerk, Volume und Container anlegen}]
# Volume für die Datenbank (überlebt Container-Neustarts)
docker volume create fitness-data

# Netzwerk (damit Backend und Frontend kommunizieren können)
docker network create fitness-net

# Backend starten
docker run -d \
  --name fitness-api \
  --network fitness-net \
  -v fitness-data:/app/data \
  -p 5000:5000 \
  fitness-api:latest

# Frontend starten
docker run -d \
  --name fitness-web \
  --network fitness-net \
  -p 80:80 \
  fitness-web:latest
\end{lstlisting}

\textbf{Optionen erklärt:}
\begin{itemize}
    \item \texttt{-d} – Detached Mode: Container läuft im Hintergrund (gibt die Konsole frei).
    \item \texttt{--name fitness-api} – Gibt dem Container einen festen Namen (sonst vergibt Docker Zufallsnamen).
    \item \texttt{--network fitness-net} – Bindet den Container in unser Docker-Netzwerk ein. Container im selben Netzwerk können sich über ihren Namen erreichen (z.B. \texttt{fitness-api}).
    \item \texttt{-v fitness-data:/app/data} – Bindet das Volume \texttt{fitness-data} in den Container-Pfad \texttt{/app/data} ein. Alles, was im Container unter \texttt{/app/data} gespeichert wird, landet tatsächlich im Volume und überlebt.
    \item \texttt{-p 5000:5000} – Port-Mapping: Leitet Port 5000 des Hosts (Server) an Port 5000 des Containers weiter.
\end{itemize}

\subsection{Aufgetretene Probleme und Lösungen}

\textbf{Problem 1: Nginx konnte Host "backend" nicht auflösen}
\begin{itemize}
    \item Ursache: In \texttt{nginx.conf} stand \texttt{proxy\_pass http://backend:5000}, aber der Backend-Container heißt \texttt{fitness-api}.
    \item Lösung: \texttt{backend} $\rightarrow$ \texttt{fitness-api} in \texttt{nginx.conf} ändern, Image neu bauen.
\end{itemize}

\textbf{Problem 2: Frontend lud Assets mit Pfad \texttt{/app/...}}
\begin{itemize}
    \item Ursache: In \texttt{vite.config.ts} war \texttt{base: "/app/"} für PWA-Zwecke gesetzt.
    \item Lösung: Geändert auf \texttt{base: "/"}.
\end{itemize}

\textbf{Problem 3: API-Anfragen gingen an lokale IP \texttt{192.168.178.189}}
\begin{itemize}
    \item Ursache: Im Client stand \texttt{const API\_BASE = "http://192.168.178.189:5107"}.
    \item Lösung: Geändert auf relative Pfade (\texttt{/api/workouts}), sodass Nginx die Anfragen per Reverse Proxy ans Backend weiterleitet.
\end{itemize}

\textbf{Problem 4: Doppeltes \texttt{/api} im Pfad}
\begin{itemize}
    \item Ursache: Client hatte \texttt{API\_BASE = "/api"} und Endpunkte begannen ebenfalls mit \texttt{/api}.
    \item Ergebnis: Anfragen gingen an \texttt{/api/api/workouts}.
    \item Lösung: API-Base auf leeren String gesetzt und Endpunkte mit \texttt{/api/} beginnen lassen.
\end{itemize}

\subsection{Zusammenfassung}

Nach diesem Schritt ist die Fitness-App produktiv auf dem VPS im Einsatz:

\begin{itemize}
    \item Die App ist unter \texttt{http://185.209.229.167} weltweit erreichbar
    \item Workouts werden persistent in einer SQLite-Datenbank gespeichert
    \item Docker-Volumes stellen sicher, dass Daten Container-Neustarts überleben
    \item Das Backend läuft auf .NET 8, das Frontend auf Nginx
    \item Ein Reverse Proxy (Nginx) leitet API-Anfragen intern an das Backend weiter
\end{itemize}