Files
fitness-app/LateX/step_07.tex
T

369 lines
14 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
% ============================================
% 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}