Initial commit: Portfolio mit LaTeX-Dokumentation
Deploy Portfolio / deploy (push) Successful in 7s

This commit is contained in:
2026-05-10 12:35:20 +02:00
commit 6f9e92c55f
36 changed files with 6311 additions and 0 deletions
+445
View File
@@ -0,0 +1,445 @@
% ============================================
% 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 (
<div className="min-h-screen bg-gray-950 text-white flex items-center justify-center">
<h1 className="text-4xl font-bold"> Portfolio</h1>
</div>
);
}
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 (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 text-white p-8">
<div className="max-w-4xl mx-auto">
<header className="text-center mb-16 pt-20">
<span className="inline-block px-4 py-1 rounded-full bg-emerald-500/20 text-emerald-300 text-sm font-medium mb-4 animate-pulse">
Portfolio 2026
</span>
<h1 className="text-7xl font-bold mb-4 bg-gradient-to-r from-emerald-400 via-cyan-400 to-purple-400 text-transparent bg-clip-text">
Mein Portfolio
</h1>
<p className="text-xl text-gray-300">
Full-Stack Entwickler & DevOps Enthusiast
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="group bg-white/5 backdrop-blur rounded-xl p-8 text-center hover:bg-white/10 transition-all duration-300 border border-white/10 hover:border-emerald-500 hover:scale-105 cursor-pointer">
<span className="text-5xl block mb-4"> </span>
<h2 className="text-2xl font-semibold group-hover:text-emerald-400 transition-colors">
Projekte
</h2>
<p className="text-gray-400 mt-2">React, .NET, Docker</p>
</div>
<div className="group bg-white/5 backdrop-blur rounded-xl p-8 text-center hover:bg-white/10 transition-all duration-300 border border-white/10 hover:border-cyan-500 hover:scale-105 cursor-pointer">
<span className="text-5xl block mb-4"> </span>
<h2 className="text-2xl font-semibold group-hover:text-cyan-400 transition-colors">
Skills
</h2>
<p className="text-gray-400 mt-2">TypeScript, C#, SQL</p>
</div>
<div className="group bg-white/5 backdrop-blur rounded-xl p-8 text-center hover:bg-white/10 transition-all duration-300 border border-white/10 hover:border-purple-500 hover:scale-105 cursor-pointer">
<span className="text-5xl block mb-4"> </span>
<h2 className="text-2xl font-semibold group-hover:text-purple-400 transition-colors">
Kontakt
</h2>
<p className="text-gray-400 mt-2">Immer erreichbar</p>
</div>
</div>
<div className="mt-16 p-8 bg-white/5 rounded-xl backdrop-blur border border-white/10">
<h3 className="text-xl font-bold mb-4"> Tailwind Farb-Test</h3>
<div className="flex flex-wrap gap-2">
{[
"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) => (
<div
key={color}
className={`w-12 h-12 rounded-lg ${color} hover:scale-125 transition-transform cursor-pointer`}
title={color}
/>
))}
</div>
</div>
</div>
</div>
);
}
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!}