369 lines
14 KiB
TeX
369 lines
14 KiB
TeX
% ============================================
|
||
% 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} |