Files
portfolio/LateX/step_01.tex
T
robre 6f9e92c55f
Deploy Portfolio / deploy (push) Successful in 7s
Initial commit: Portfolio mit LaTeX-Dokumentation
2026-05-10 12:35:20 +02:00

445 lines
16 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
% ============================================
% 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!}