This commit is contained in:
@@ -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!}
|
||||
Reference in New Issue
Block a user