% ============================================
% STEP 07: CI/CD MIT GITEA ACTIONS - DAS VOLLSTÄNDIGE TUTORIAL
% ============================================

\section{CI/CD mit Gitea Actions}
\label{sec:step07}

In diesem Schritt haben wir eine vollständige CI/CD-Pipeline mit Gitea Actions eingerichtet. Bei jedem \texttt{git push} auf den \texttt{master}-Branch wird unsere Anwendung automatisch gebaut und auf dem Server deployed. Dieses Kapitel dokumentiert den kompletten Prozess, alle aufgetretenen Probleme und deren Lösungen, sowie ein ausführliches Docker-Tutorial für die tägliche Arbeit.

\subsection{Docker-Grundlagen: Container verstehen und verwalten}

Bevor wir in die CI/CD-Pipeline einsteigen, ist es wichtig, die Docker-Befehle zu verstehen, mit denen wir täglich arbeiten.

\subsubsection{Container auflisten}

\begin{lstlisting}[language=Bash, caption={Alle laufenden Container anzeigen}]
docker ps
\end{lstlisting}

\textbf{Ausgabe verstehen:}

\begin{verbatim}
CONTAINER ID   IMAGE                       PORTS                  NAMES
98f3eff6a77f   gitea/gitea:latest          0.0.0.0:3000->3000    gitea
e8e823a7beea   nginxproxy/nginx-proxy      0.0.0.0:80->80        nginx-proxy
596b718e9c16   fitness-api:latest          5000/tcp               fitness-api
\end{verbatim}

\textbf{Spalten erklärt:}
\begin{itemize}
    \item \texttt{CONTAINER ID} – Eindeutige 12-stellige Hexadezimal-ID des Containers. Kann abgekürzt verwendet werden (z.B. \texttt{98f3}).
    \item \texttt{IMAGE} – Das Image, aus dem der Container gestartet wurde. \texttt{latest} ist der Tag (Version).
    \item \texttt{PORTS} – Port-Weiterleitungen. \texttt{0.0.0.0:3000->3000} bedeutet: Port 3000 des Hosts ist auf Port 3000 des Containers weitergeleitet. Steht nur \texttt{5000/tcp}, ist der Port nur container-intern erreichbar.
    \item \texttt{NAMES} – Der Name, den wir dem Container gegeben haben. Wird für \texttt{docker exec} und \texttt{docker logs} verwendet.
\end{itemize}

\textbf{Alle Container (auch gestoppte):}
\begin{lstlisting}[language=Bash, caption={Auch gestoppte Container anzeigen}]
docker ps -a
\end{lstlisting}

\subsubsection{Container-Logs anzeigen}

\begin{lstlisting}[language=Bash, caption={Logs eines Containers anzeigen}]
# Letzte 50 Zeilen
docker logs fitness-api --tail 50

# Logs live verfolgen (Strg+C zum Beenden)
docker logs -f nginx-proxy

# Logs seit 10 Minuten
docker logs fitness-web --since 10m
\end{lstlisting}

\textbf{Typische Log-Einträge verstehen:}
\begin{itemize}
    \item \texttt{info: Microsoft.Hosting.Lifetime[14] Now listening on: http://[::]:5000} – Backend läuft und wartet auf Anfragen
    \item \texttt{[notice] 19\#19: signal 1 (SIGHUP) received, reconfiguring} – nginx wurde neu geladen (nach Konfigurationsänderung)
    \item \texttt{ERROR: failed to build: failed to solve} – Docker-Build-Fehler (häufig falsche Pfade)
\end{itemize}

\subsubsection{Container stoppen, starten, neustarten}

\begin{lstlisting}[language=Bash, caption={Container-Lebenszyklus}]
# Container stoppen (bleibt existent, kann neugestartet werden)
docker stop fitness-web

# Gestoppten Container wieder starten
docker start fitness-web

# Container neustarten (stop + start)
docker restart nginx-proxy

# Container stoppen UND löschen (Wegwerfen!)
docker rm fitness-web

# Erzwingen: stoppen + löschen in einem Befehl
docker rm -f fitness-web
\end{lstlisting}

\subsubsection{In einen Container einsteigen}

\begin{lstlisting}[language=Bash, caption={Shell im Container öffnen}]
# In einen laufenden Container einsteigen und Bash öffnen
docker exec -it fitness-api bash

# Einzelnen Befehl im Container ausführen
docker exec nginx-proxy nginx -s reload

# Mit sh (falls bash nicht verfügbar)
docker exec -it fitness-web sh
\end{lstlisting}

\textbf{Erklärung:}
\begin{itemize}
    \item \texttt{exec} – Führt einen Befehl in einem laufenden Container aus
    \item \texttt{-it} – Interaktiv + Terminal (damit du tippen kannst)
    \item \texttt{bash / sh} – Die zu öffnende Shell
\end{itemize}

\subsubsection{Images verwalten}

\begin{lstlisting}[language=Bash, caption={Images anzeigen und löschen}]
# Alle lokal gespeicherten Images anzeigen
docker images

# Nicht mehr verwendete Images löschen
docker image prune -a

# Ein bestimmtes Image löschen
docker rmi fitness-api:latest
\end{lstlisting}

\subsubsection{Netzwerke inspizieren}

\begin{lstlisting}[language=Bash, caption={Netzwerke untersuchen}]
# Alle Docker-Netzwerke
docker network ls

# Details eines Netzwerks (welche Container sind drin?)
docker network inspect fitness_proxy-net

# Nur die Container-Namen im Netzwerk anzeigen
docker network inspect fitness_proxy-net --format='{{range .Containers}}{{.Name}} {{end}}'
\end{lstlisting}

\subsection{CI/CD-Pipeline mit Gitea Actions einrichten}

\subsubsection{Was ist CI/CD?}

\textbf{CI (Continuous Integration):} Bei jedem Push wird der Code automatisch gebaut und getestet. Fehler werden sofort erkannt.

\textbf{CD (Continuous Deployment):} Nach erfolgreichem Build wird die Anwendung automatisch auf dem Server deployed.

\textbf{Unser Workflow:}
\begin{enumerate}
    \item Developer macht \texttt{git push} auf den \texttt{master}-Branch
    \item Gitea erkennt den Push und sucht nach Workflow-Dateien (\texttt{.gitea/workflows/*.yaml})
    \item Gitea weist den Job dem \texttt{gitea\_runner} zu
    \item Der Runner checkt den Code aus, baut Docker-Images und startet die Container neu
\end{enumerate}

\subsubsection{Der Gitea Act Runner}

Der Runner ist der "Arbeiter", der die CI/CD-Jobs ausführt. Er läuft als Docker-Container und hat Zugriff auf den Docker-Socket des Hosts, um selbst Container zu bauen.

\begin{lstlisting}[language=Bash, caption={Runner starten und mit Gitea verbinden}]
docker run -d --name gitea_runner \
  -e GITEA_INSTANCE_URL=http://185.209.229.167:3000 \
  -e GITEA_RUNNER_REGISTRATION_TOKEN=DEIN_TOKEN \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitea/act_runner:nightly
\end{lstlisting}

\textbf{Wichtige Details:}
\begin{itemize}
    \item \texttt{-v /var/run/docker.sock:/var/run/docker.sock} – Gibt dem Runner Zugriff auf Docker. \textbf{Ohne diese Zeile kann der Runner keine Images bauen!}
    \item Der Token ist ein Einmal-Passwort. Nach erfolgreicher Registrierung ist er verbraucht. Der Container stoppt dann mit Exit-Code 0 – das ist normal! Einfach \texttt{docker start gitea\_runner} ausführen.
\end{itemize}

\subsubsection{Die Workflow-Datei}

Die Datei \texttt{.gitea/workflows/deploy.yaml} definiert den Ablauf:

\begin{lstlisting}[language=YAML, caption={Vollständige deploy.yaml (finale Version)}]
name: Deploy Fitness App
on:
  push:
    branches: [ "master" ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    container:
      image: catthehacker/ubuntu:act-latest
      volumes:
        - /opt/fitness:/opt/fitness

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build & Deploy
        run: |
          docker build -f Dockerfile.api -t fitness-api:latest .
          docker build -f Dockerfile.web -t fitness-web:latest .
          docker stop fitness-api fitness-web 2>/dev/null || true
          docker rm fitness-api fitness-web 2>/dev/null || true
          docker run -d --name fitness-api --network fitness_proxy-net \
            -v fitness_fitness-data:/app/data fitness-api:latest
          docker run -d --name fitness-web --network fitness_proxy-net \
            -p 80:80 \
            -e VIRTUAL_HOST=robre.de,www.robre.de \
            -e LETSENCRYPT_HOST=robre.de,www.robre.de \
            -e VIRTUAL_PORT=80 \
            fitness-web:latest
\end{lstlisting}

\textbf{Zeile für Zeile erklärt:}
\begin{itemize}
    \item \texttt{on: push: branches: [ "master" ]} – Der Workflow startet nur bei Pushes auf den master-Branch.
    \item \texttt{runs-on: ubuntu-latest} – Die virtuelle Maschine, auf der der Job läuft.
    \item \texttt{container: image: catthehacker/ubuntu:act-latest} – Ein Docker-Image mit Ubuntu + Docker CLI.
    \item \texttt{volumes: /opt/fitness:/opt/fitness} – Bindet das Projektverzeichnis ein.
    \item \texttt{2>/dev/null || true} – Unterdrückt Fehlermeldungen, wenn die Container nicht existieren.
\end{itemize}

\subsection{Alle aufgetretenen Probleme und ihre Lösungen}

\subsubsection{Problem 1: Docker-Socket doppelt gemountet}
\textbf{Fehlermeldung:} \texttt{Duplicate mount point: /var/run/docker.sock}

\textbf{Ursache:} Der Gitea-Runner mountet den Docker-Socket automatisch. Wir hatten ihn zusätzlich im Workflow definiert.

\textbf{Lösung:} Die Zeile \texttt{- /var/run/docker.sock:/var/run/docker.sock} aus dem Workflow entfernen.

\subsubsection{Problem 2: Pfade zu Dockerfiles}
\textbf{Fehlermeldung:} \texttt{open Dockerfile: no such file or directory}

\textbf{Ursache:} Die Dockerfiles lagen in \texttt{apps/api/} und \texttt{apps/web/}, aber der Build-Kontext war das Repository-Root. Relative Pfade funktionierten nicht.

\textbf{Lösung:} Kopien der Dockerfiles (\texttt{Dockerfile.api}, \texttt{Dockerfile.web}) im Root-Verzeichnis angelegt.

\subsubsection{Problem 3: ERR\_PNPM\_IGNORED\_BUILDS}
\textbf{Fehlermeldung:} \texttt{[ERR\_PNPM\_IGNORED\_BUILDS] Ignored build scripts: @swc/core}

\textbf{Ursache:} pnpm verweigert standardmäßig Build-Scripts für Sicherheitsüberprüfung.

\textbf{Lösung:} \texttt{@vitejs/plugin-react-swc} durch \texttt{@vitejs/plugin-react} ersetzt. Keine Build-Scripts mehr nötig.

\subsubsection{Problem 4: Fehlende nginx.conf}
\textbf{Fehlermeldung:} \texttt{"/apps/web/nginx.conf": not found}

\textbf{Ursache:} Die \texttt{nginx.conf} war nicht im Docker-Build-Kontext.

\textbf{Lösung:} \texttt{nginx.conf} ins Root kopiert und \texttt{Dockerfile.web} entsprechend angepasst.

\subsubsection{Problem 5: Container ohne Port-Mapping}
\textbf{Symptom:} \texttt{docker ps} zeigt \texttt{80/tcp} ohne \texttt{0.0.0.0:80->80/tcp}.

\textbf{Ursache:} Die CI/CD-Pipeline startete Container ohne \texttt{-p 80:80}.

\textbf{Lösung:} In der Workflow-Datei explizit \texttt{-p 80:80} zum \texttt{docker run}-Befehl hinzugefügt.

\subsection{Tutorial: Einfache HTML-Seite deployen}

Um den kompletten Prozess von A bis Z zu verstehen, deployen wir eine minimale HTML-Seite.

\subsubsection{Schritt 1: Projekt erstellen}

\begin{lstlisting}[language=Bash, caption={Neues Projekt lokal anlegen}]
cd ~/projects
mkdir hello-ci
cd hello-ci
git init
\end{lstlisting}

\subsubsection{Schritt 2: index.html erstellen}

\begin{lstlisting}[language=HTML, caption={Minimale HTML-Seite}]
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Hello CI/CD</title>
    <style>
        body { font-family: sans-serif; text-align: center; padding: 50px; }
        h1 { color: #059669; }
    </style>
</head>
<body>
    <h1>Hello CI/CD!</h1>
    <p>Diese Seite wurde automatisch deployed.</p>
</body>
</html>
\end{lstlisting}

\subsubsection{Schritt 3: Dockerfile erstellen}

\begin{lstlisting}[language=Dockerfile, caption={Dockerfile für statische HTML-Seite}]
FROM nginx:stable-alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
\end{lstlisting}

\subsubsection{Schritt 4: Workflow erstellen}

\begin{lstlisting}[language=Bash, caption={Verzeichnis anlegen}]
mkdir -p .gitea/workflows
\end{lstlisting}

\begin{lstlisting}[language=YAML, caption={.gitea/workflows/deploy.yaml}]
name: Deploy Hello Page
on:
  push:
    branches: [ "master" ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    container:
      image: catthehacker/ubuntu:act-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Build and Deploy
        run: |
          docker build -t hello-ci:latest .
          docker stop hello-ci 2>/dev/null || true
          docker rm hello-ci 2>/dev/null || true
          docker run -d --name hello-ci -p 8080:80 hello-ci:latest
\end{lstlisting}

\subsubsection{Schritt 5: In Gitea pushen}

\begin{lstlisting}[language=Bash, caption={Push zu Gitea}]
# In Gitea ein neues Repository "hello-ci" anlegen, dann:
git remote add gitea http://185.209.229.167:3000/robre/hello-ci.git
git add .
git commit -m "Initial commit"
git push gitea master
\end{lstlisting}

\subsubsection{Schritt 6: Firewall öffnen und testen}

\begin{lstlisting}[language=Bash, caption={Port 8080 freigeben}]
ssh testserver "ufw allow 8080/tcp"
\end{lstlisting}

Im Browser: \texttt{http://185.209.229.167:8080}

\subsection{Docker-Befehle Cheat Sheet}

\begin{table}[h]
\centering
\caption{Häufig verwendete Docker-Befehle}
\begin{tabular}{@{}lp{8cm}@{}}
\toprule
\textbf{Befehl} & \textbf{Beschreibung} \\
\midrule
\texttt{docker ps} & Laufende Container anzeigen \\
\texttt{docker ps -a} & Alle Container (auch gestoppte) \\
\texttt{docker logs NAME} & Logs eines Containers anzeigen \\
\texttt{docker logs -f NAME} & Logs live verfolgen \\
\texttt{docker stop NAME} & Container stoppen \\
\texttt{docker start NAME} & Container starten \\
\texttt{docker restart NAME} & Container neustarten \\
\texttt{docker rm NAME} & Container löschen \\
\texttt{docker rm -f NAME} & Container erzwingen löschen \\
\texttt{docker exec -it NAME bash} & Shell im Container öffnen \\
\texttt{docker images} & Alle Images anzeigen \\
\texttt{docker network ls} & Netzwerke anzeigen \\
\texttt{docker network inspect NETZ} & Netzwerk-Details \\
\texttt{docker system prune -a} & Ungenutzte Daten löschen \\
\bottomrule
\end{tabular}
\end{table}

\subsection{Zusammenfassung}

Nach diesem Schritt haben wir:
\begin{itemize}
    \item Eine vollständige CI/CD-Pipeline mit Gitea Actions
    \item Automatisches Build und Deployment bei jedem Push auf master
    \item Verständnis aller Docker-Befehle für die tägliche Arbeit
    \item Ein komplettes Tutorial zum Nachvollziehen des Prozesses
    \item Alle Fehler dokumentiert mit Ursachen und Lösungen
\end{itemize}