Add comprehensive tutorial for CI/CD with Gitea Actions and Docker

This commit is contained in:
2026-05-09 14:12:57 +02:00
parent a806ace6d0
commit 5a20a14720
9 changed files with 825 additions and 296 deletions
+369
View File
@@ -0,0 +1,369 @@
% ============================================
% 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}