% ============================================ % 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}]
Diese Seite wurde automatisch deployed.
\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}