% ============================================ % STEP 01: PORTFOLIO-SEITE - VON NULL ZUM LIVE-DEPLOYMENT % ============================================ \section{Portfolio-Seite: Von Null zum Live-Deployment} \label{sec:step01} In diesem Tutorial bauen wir eine komplette Portfolio-Webseite mit React, Vite und Tailwind CSS – von der ersten Codezeile bis zur automatisch deployten Live-Seite per Gitea CI/CD. \subsection{Projektstruktur (Monorepo mit pnpm)} Wir verwenden ein \textbf{Monorepo} mit \texttt{pnpm} als Package-Manager. Das ermöglicht, mehrere Projekte (Frontend, Backend) in einem Repository zu verwalten. \textbf{Ordnerstruktur nach diesem Schritt:} \begin{verbatim} portfolio/ ├── apps/ │ └── web/ # React-Frontend mit Vite + Tailwind │ ├── src/ │ │ ├── App.tsx # Hauptkomponente │ │ ├── index.css # Tailwind-Import │ │ └── main.tsx # Einstiegspunkt │ ├── package.json # Frontend-Abhängigkeiten │ └── vite.config.ts # Vite + Tailwind Konfiguration ├── .gitea/ │ └── workflows/ │ └── deploy.yaml # CI/CD Pipeline ├── .gitignore ├── .npmrc # pnpm Build-Scripts erlauben ├── Dockerfile # Docker-Build für Produktion ├── package.json # Root-Konfiguration ├── pnpm-lock.yaml # Lockfile (automatisch erstellt) └── pnpm-workspace.yaml # Workspace-Definition \end{verbatim} \subsection{Schritt 1: Monorepo initialisieren} \begin{lstlisting}[language=Bash, caption={Monorepo mit pnpm einrichten}] cd ~/projects mkdir portfolio cd portfolio # pnpm initialisieren pnpm init # Workspace-Struktur definieren cat > pnpm-workspace.yaml << 'EOF' packages: - "apps/*" EOF # Root package.json anpassen cat > package.json << 'EOF' { "name": "portfolio", "version": "1.0.0", "private": true, "scripts": { "dev": "pnpm --filter web dev", "build": "pnpm --filter web build" } } EOF # Apps-Ordner für das Frontend mkdir -p apps/web \end{lstlisting} \textbf{Erklärung der Dateien:} \begin{itemize} \item \texttt{pnpm-workspace.yaml} – Teilt pnpm mit, dass alle Ordner unter \texttt{apps/} eigenständige Pakete sind \item \texttt{package.json} – Root-Konfiguration mit praktischen Scripts. \texttt{--filter web} führt den Befehl nur im \texttt{apps/web}-Paket aus \end{itemize} \subsection{Schritt 2: React + Vite + TypeScript einrichten} \begin{lstlisting}[language=Bash, caption={Vite-Projekt erstellen}] cd ~/projects/portfolio # Vite-Projekt mit React und TypeScript erstellen pnpm create vite apps/web --template react-swc-ts # In den web-Ordner wechseln cd apps/web # Abhängigkeiten installieren pnpm install \end{lstlisting} \textbf{Erklärung:} \begin{itemize} \item \texttt{pnpm create vite} – Erstellt ein neues Vite-Projekt im angegebenen Ordner \item \texttt{--template react-swc-ts} – Verwendet die Vorlage mit React, SWC (schneller Compiler) und TypeScript \item \texttt{pnpm install} – Installiert alle Abhängigkeiten aus \texttt{package.json} \end{itemize} \subsection{Schritt 3: Tailwind CSS einrichten} \begin{lstlisting}[language=Bash, caption={Tailwind CSS installieren und konfigurieren}] cd ~/projects/portfolio/apps/web # Tailwind-Pakete installieren pnpm add tailwindcss @tailwindcss/vite # vite.config.ts überschreiben cat > vite.config.ts << 'EOF' import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [react(), tailwindcss()], }); EOF # index.css anpassen (nur Tailwind-Import) cat > src/index.css << 'EOF' @import "tailwindcss"; EOF \end{lstlisting} \textbf{Erklärung:} \begin{itemize} \item \texttt{tailwindcss} – Das Tailwind CSS Framework (Version 4) \item \texttt{@tailwindcss/vite} – Das offizielle Vite-Plugin für Tailwind CSS. Es verarbeitet die Tailwind-Klassen direkt beim Build. \item \texttt{@import "tailwindcss"} – Importiert alle Tailwind-Basis-Styles, Komponenten und Utilities \end{itemize} \textbf{Wichtig:} Tailwind 4 verwendet \texttt{@import "tailwindcss"} statt der alten \texttt{@tailwind base/components/utilities}-Direktiven. Kein \texttt{tailwind.config.js} mehr nötig! \subsection{Schritt 4: Erste App-Komponente} \begin{lstlisting}[language=Bash, caption={Minimale App.tsx}] cat > src/App.tsx << 'EOF' function App() { return (

Portfolio

); } export default App; EOF \end{lstlisting} \textbf{Lokal testen:} \begin{lstlisting}[language=Bash, caption={Entwicklungsserver starten}] cd ~/projects/portfolio pnpm run dev \end{lstlisting} Im Browser: \texttt{http://localhost:5173} \subsection{Schritt 5: Git initialisieren} \begin{lstlisting}[language=Bash, caption={Git Repository einrichten}] cd ~/projects/portfolio # .gitignore erstellen cat > .gitignore << 'EOF' node_modules dist .vs .idea *.db EOF # Git initialisieren und ersten Commit machen git init git add . git commit -m "Initial commit: Monorepo mit React + Vite + Tailwind" \end{lstlisting} \subsection{Schritt 6: Gitea-Repository anlegen und pushen} \begin{enumerate} \item Im Browser \texttt{http://185.209.229.167:3000} öffnen \item Rechts oben auf \textbf{+} → \textbf{New Repository} \item Name: \texttt{portfolio}, auf \textbf{Create Repository} klicken \end{enumerate} \begin{lstlisting}[language=Bash, caption={Gitea als Remote hinzufügen und pushen}] git remote add gitea http://185.209.229.167:3000/robre/portfolio.git git push gitea master \end{lstlisting} \textbf{Credential Helper (damit Git sich Username/Passwort merkt):} \begin{lstlisting}[language=Bash, caption={Git Credential Helper aktivieren}] git config --global credential.helper store \end{lstlisting} Beim nächsten Push einmalig Username (\texttt{robre}) und Gitea-Passwort eingeben – danach nie wieder. \subsection{Schritt 7: Dockerfile für Produktion} Da das Portfolio nur aus statischen Dateien besteht (nach dem Vite-Build), brauchen wir einen zweistufigen Docker-Build: \begin{lstlisting}[language=Dockerfile, caption={Dockerfile für das Portfolio}] FROM node:22-alpine AS build WORKDIR /app COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc ./ COPY apps/web/package.json apps/web/ RUN npm install -g pnpm && pnpm install --no-frozen-lockfile COPY apps/web/ apps/web/ WORKDIR /app/apps/web RUN pnpm run build FROM nginx:stable-alpine COPY --from=build /app/apps/web/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] \end{lstlisting} \textbf{Zeile für Zeile erklärt:} \begin{itemize} \item \texttt{FROM node:22-alpine AS build} – Leichtes Node.js-Image für den Build \item \texttt{COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc ./} – Konfigurationsdateien für pnpm. Ohne \texttt{pnpm-workspace.yaml} findet pnpm die Pakete nicht! \item \texttt{COPY apps/web/package.json apps/web/} – Nur package.json zuerst kopieren (Docker-Cache für schnellere Builds) \item \texttt{RUN npm install -g pnpm \&\& pnpm install --no-frozen-lockfile} – pnpm installieren und Abhängigkeiten installieren \item \texttt{COPY apps/web/ apps/web/} – Restlichen Code kopieren \item \texttt{WORKDIR /app/apps/web} – Ins Frontend-Verzeichnis wechseln \item \texttt{RUN pnpm run build} – Produktions-Build mit Vite (erstellt \texttt{dist/}) \item \texttt{FROM nginx:stable-alpine} – Neues, schlankes Image für den Webserver \item \texttt{COPY --from=build /app/apps/web/dist /usr/share/nginx/html} – Nur den Build-Output kopieren \item \texttt{EXPOSE 80 / CMD ["nginx", "-g", "daemon off;"]} – Nginx starten \end{itemize} \subsection{Schritt 8: .npmrc für Build-Scripts} \begin{lstlisting}[language=Bash, caption={Build-Scripts erlauben}] cat > .npmrc << 'EOF' pnpm.onlyBuiltDependencies=* EOF \end{lstlisting} \textbf{Erklärung:} pnpm blockt standardmäßig Build-Scripts aus Sicherheitsgründen. Diese Datei erlaubt alle Build-Scripts – notwendig für Pakete wie \texttt{@swc/core} oder \texttt{esbuild}. \subsection{Schritt 9: CI/CD-Pipeline mit Gitea Actions} \begin{lstlisting}[language=Bash, caption={Workflow-Ordner erstellen}] mkdir -p .gitea/workflows \end{lstlisting} \begin{lstlisting}[language=YAML, caption={.gitea/workflows/deploy.yaml}] name: Deploy Portfolio 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 portfolio:latest . docker stop portfolio 2>/dev/null || true docker rm portfolio 2>/dev/null || true docker run -d --name portfolio -p 8081:80 portfolio:latest \end{lstlisting} \textbf{Erklärung:} \begin{itemize} \item \texttt{on: push: branches: ["master"]} – Der Workflow läuft bei jedem Push auf master \item \texttt{runs-on: ubuntu-latest} – Virtuelle Maschine für den Job \item \texttt{container: image: catthehacker/ubuntu:act-latest} – Docker-Image mit Ubuntu + Docker CLI \item \texttt{actions/checkout@v4} – Checkt den Code aus dem Repository aus \item \texttt{docker build -t portfolio:latest .} – Baut das Docker-Image \item \texttt{docker stop/rm 2>/dev/null || true} – Stoppt alten Container (ignoriert Fehler, falls nicht existiert) \item \texttt{docker run -d --name portfolio -p 8081:80 portfolio:latest} – Startet neuen Container auf Port 8081 \end{itemize} \subsection{Schritt 10: Firewall öffnen und deployen} \begin{lstlisting}[language=Bash, caption={Port 8081 freigeben}] ssh testserver "ufw allow 8081/tcp" \end{lstlisting} \begin{lstlisting}[language=Bash, caption={Alles pushen – löst Pipeline aus!}] git add . git commit -m "Dockerfile + CI/CD Pipeline hinzugefügt" git push gitea master \end{lstlisting} \subsection{Aufgetretene Fehler und ihre Lösungen} \subsubsection{Fehler 1: pnpm-workspace.yaml not found} \textbf{Fehlermeldung:} \texttt{"/pnpm-workspace.yaml": not found} \textbf{Ursache:} Die Datei wurde nie erstellt, weil \texttt{git init} zurückgesetzt wurde. \textbf{Lösung:} \texttt{pnpm-workspace.yaml} manuell erstellen und committen. \subsubsection{Fehler 2: pnpm-lock.yaml not found} \textbf{Fehlermeldung:} \texttt{"/pnpm-lock.yaml": not found} \textbf{Ursache:} \texttt{pnpm install} wurde nie im Root ausgeführt, daher kein Lockfile. \textbf{Lösung:} \texttt{pnpm install} im Root ausführen, dann die erstellte \texttt{pnpm-lock.yaml} committen. \subsubsection{Fehler 3: ERR\_PNPM\_IGNORED\_BUILDS} \textbf{Fehlermeldung:} \texttt{[ERR\_PNPM\_IGNORED\_BUILDS] Ignored build scripts} \textbf{Ursache:} pnpm blockt Build-Scripts aus Sicherheitsgründen. \textbf{Lösung:} \texttt{.npmrc} mit \texttt{pnpm.onlyBuiltDependencies=*} erstellen. \subsection{Die Seite erreichen} \textbf{Im Browser:} \begin{lstlisting}[language=Bash, caption={Portfolio-URL}] http://185.209.229.167:8081 \end{lstlisting} \textbf{Container-Status prüfen:} \begin{lstlisting}[language=Bash, caption={Container-Check}] ssh testserver "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep portfolio" \end{lstlisting} \textbf{Erwartete Ausgabe:} \begin{verbatim} portfolio Up 2 minutes 0.0.0.0:8081->80/tcp \end{verbatim} \textbf{Port-Mapping lesen:} \begin{itemize} \item \texttt{0.0.0.0:8081->80/tcp} – Von außen über Port 8081 erreichbar, intern läuft Nginx auf Port 80 \item Die \texttt{0.0.0.0} bedeutet: Auf ALLEN Netzwerkschnittstellen des Servers (IPv4 und IPv6) \end{itemize} \subsection{Vollständiger Code: App.tsx mit Tailwind} \begin{lstlisting}[language=TypeScript, caption={Vollständige App.tsx mit Tailwind-Styling}] function App() { return (
Portfolio 2026

Mein Portfolio

Full-Stack Entwickler & DevOps Enthusiast

Projekte

React, .NET, Docker

Skills

TypeScript, C#, SQL

Kontakt

Immer erreichbar

Tailwind Farb-Test

{[ "bg-red-500", "bg-orange-500", "bg-yellow-500", "bg-green-500", "bg-emerald-500", "bg-cyan-500", "bg-blue-500", "bg-purple-500", "bg-pink-500", "bg-rose-500" ].map((color) => (
))}
); } export default App; \end{lstlisting} \subsection{Tailwind-Klassen im Überblick} \begin{table}[h] \centering \caption{Verwendete Tailwind-Klassen und ihre Bedeutung} \begin{tabular}{@{}lp{7cm}@{}} \toprule \textbf{Klasse} & \textbf{Bedeutung} \\ \midrule \texttt{min-h-screen} & Mindesthöhe = Bildschirmhöhe \\ \texttt{bg-gradient-to-br} & Hintergrund-Farbverlauf von oben-links nach unten-rechts \\ \texttt{from-/via-/to-COLOR} & Farben des Farbverlaufs \\ \texttt{text-transparent bg-clip-text} & Text mit Farbverlauf füllen \\ \texttt{backdrop-blur} & Hintergrund-Weichzeichner (Glassmorphismus) \\ \texttt{bg-white/5} & Weiß mit 5\% Deckkraft \\ \texttt{group} & Parent für Gruppen-Hover-Effekte \\ \texttt{group-hover:text-COLOR} & Textfarbe ändert sich bei Hover auf Parent \\ \texttt{group-hover:scale-105} & Vergrößerung bei Hover auf Parent \\ \texttt{transition-all duration-300} & Sanfte Übergänge über 300ms \\ \texttt{animate-pulse} & Pulsierende Animation \\ \bottomrule \end{tabular} \end{table} \subsection{Zusammenfassung} In diesem Tutorial haben wir: \begin{itemize} \item Ein Monorepo mit pnpm Workspace eingerichtet \item React + Vite + TypeScript + Tailwind CSS 4 installiert \item Ein Dockerfile für den Produktions-Build erstellt \item Eine CI/CD-Pipeline mit Gitea Actions konfiguriert \item Die Seite automatisch bei jedem Push deployed \item Drei typische Fehler analysiert und behoben \item Eine vollständige Portfolio-Landingpage mit Tailwind gestaltet \end{itemize} \textbf{Die Seite ist live unter:} \texttt{http://185.209.229.167:8081} \textbf{Die Pipeline läuft bei jedem Push auf master automatisch!}