Add steps for domain purchase and DNS configuration, set up HTTPS with nginx-proxy and Let's Encrypt, and install Gitea as a self-hosted Git server. Remove temporary database files and update assembly info for API project.

This commit is contained in:
2026-05-08 14:53:33 +02:00
parent 0e9377739e
commit b8cfa1689f
15 changed files with 1285 additions and 141 deletions
+323
View File
@@ -0,0 +1,323 @@
\section{HTTPS mit nginx-proxy und Let's Encrypt}
\label{sec:step05}
In diesem Schritt richten wir \textbf{HTTPS} für unsere Domain ein. Dafür nutzen wir zwei Docker-Container: \texttt{nginx-proxy} als Reverse Proxy und \texttt{acme-companion} für automatische SSL-Zertifikate von Let's Encrypt. Nach diesem Schritt ist unsere App unter \texttt{https://robre.de} erreichbar und erfüllt alle Voraussetzungen für die PWA-Installation.
\subsection{Warum brauchen wir HTTPS?}
HTTP (Hypertext Transfer Protocol) sendet alle Daten im \textbf{Klartext}. Ein Angreifer im gleichen Netzwerk kann mitlesen, manipulieren oder die Verbindung kapern. HTTPS (HTTP Secure) verschlüsselt die gesamte Kommunikation zwischen Browser und Server mittels TLS (Transport Layer Security).
\textbf{Für Progressive Web Apps ist HTTPS zwingend vorgeschrieben:}
\begin{itemize}
\item Der \textbf{Service Worker} das Herzstück einer PWA funktioniert nur mit HTTPS
\item Browser verweigern die PWA-Installation bei unverschlüsselten Verbindungen
\item Moderne Browser-APIs (Geolocation, Kamera, Push-Benachrichtigungen) erfordern HTTPS
\end{itemize}
\subsection{Wie funktioniert das SSL-Zertifikat von Let's Encrypt?}
\textbf{Let's Encrypt} ist eine kostenlose Zertifizierungsstelle (Certificate Authority). Der Ablauf der Zertifikatserstellung läuft automatisch nach dem \textbf{ACME-Protokoll} (Automatic Certificate Management Environment):
\begin{enumerate}
\item Der \texttt{acme-companion} fordert ein Zertifikat für \texttt{robre.de} an
\item Let's Encrypt stellt eine \textbf{Challenge} eine Aufgabe, die beweist, dass du die Domain kontrollierst
\item Der \texttt{acme-companion} legt eine temporäre Datei auf deinem Webserver ab
\item Let's Encrypt prüft, ob diese Datei unter \texttt{http://robre.de/.well-known/acme-challenge/...} erreichbar ist
\item Wenn ja: Zertifikat wird ausgestellt und automatisch in den nginx eingebunden
\item Das Zertifikat ist 90 Tage gültig und wird automatisch erneuert
\end{enumerate}
\textbf{Wichtige Voraussetzung:} Die Domain muss weltweit über DNS erreichbar sein (\textbf{DNS-Propagation}). Ohne funktionierende DNS-Auflösung schlägt die Challenge fehl Let's Encrypt kann die Domain nicht finden und verweigert die Zertifikatsausstellung.
\subsection{DNS-Propagation: Wie lange dauert es?}
Wenn du eine neue Domain registrierst oder DNS-Einträge änderst, dauert es eine Weile, bis alle DNS-Server weltweit die neuen Informationen kennen. Diesen Prozess nennt man \textbf{DNS-Propagation}.
\begin{itemize}
\item \textbf{Normale Dauer:} 15 Minuten bis 2 Stunden
\item \textbf{Maximale Dauer:} Bis zu 48 Stunden (in seltenen Fällen)
\item \textbf{Beeinflussender Faktor:} Der \textbf{TTL-Wert} (Time To Live) der DNS-Einträge. Unser Wert von 86400 Sekunden (24 Stunden) erlaubt anderen DNS-Servern, die Information bis zu 24 Stunden zwischenzuspeichern.
\end{itemize}
\textbf{So prüfst du den Status der Propagation:}
\begin{lstlisting}[language=Bash, caption={DNS-Auflösung lokal prüfen}]
# Über deinen Standard-DNS-Resolver
nslookup robre.de
# Direkt bei Contabos Nameserver (ohne Cache)
nslookup robre.de ns1.contabo.net
# Über Googles öffentlichen DNS (8.8.8.8)
nslookup robre.de 8.8.8.8
\end{lstlisting}
Erscheint als Antwort \texttt{185.209.229.167}, ist die Propagation für diesen Server abgeschlossen. Erscheint \texttt{NXDOMAIN} (Non-Existent Domain), kennt der DNS-Server die Domain noch nicht.
\textbf{Online-Tool zur weltweiten Prüfung:} \texttt{https://dnschecker.org} zeigt auf einer Weltkarte, an welchen Standorten die Domain bereits aufgelöst wird.
\subsection{Architektur: Wie hängen die Container zusammen?}
Nach diesem Schritt sieht unsere Docker-Infrastruktur so aus:
\begin{verbatim}
Internet
|
| HTTPS (Port 443)
v
nginx-proxy (Reverse Proxy)
|
+-- robre.de --> fitness-web (Port 80)
| |
| | /api/* --> fitness-api (Port 5000)
|
+-- SSL-Zertifikate von acme-companion verwaltet
\end{verbatim}
\textbf{Erklärung der Komponenten:}
\begin{itemize}
\item \texttt{nginx-proxy}: Empfängt alle HTTP/HTTPS-Anfragen und leitet sie an den richtigen Container weiter. Entscheidet anhand des \texttt{Host}-Headers (enthält die Domain), welcher Container die Anfrage bekommt.
\item \texttt{acme-companion}: Überwacht laufende Container auf die Umgebungsvariable \texttt{LETSENCRYPT\_HOST}. Wenn eine neue Domain auftaucht, fordert er automatisch ein SSL-Zertifikat an und konfiguriert nginx.
\item \texttt{fitness-web}: Unser React-Frontend mit Nginx. Braucht selbst kein SSL der Proxy übernimmt die Verschlüsselung.
\item \texttt{fitness-api}: Unser .NET-Backend. Nur über das interne Docker-Netzwerk erreichbar, nicht direkt von außen.
\end{itemize}
\subsection{Schritt-für-Schritt: HTTPS einrichten}
\subsubsection{Schritt 1: Bestehende Container stoppen und löschen}
Da wir von manuellen \texttt{docker run}-Befehlen auf \texttt{docker compose} umsteigen, müssen die alten Container entfernt werden.
\begin{lstlisting}[language=Bash, caption={Alte Container stoppen und löschen}]
ssh testserver
docker stop fitness-web fitness-api
docker rm fitness-web fitness-api
\end{lstlisting}
\subsubsection{Schritt 2: Verzeichnis für docker-compose anlegen}
\begin{lstlisting}[language=Bash, caption={Projektverzeichnis auf dem Server}]
mkdir -p /opt/fitness
cd /opt/fitness
\end{lstlisting}
\subsubsection{Schritt 3: docker-compose.yml erstellen}
\texttt{docker compose} ist ein Tool, mit dem wir mehrere Container als \textbf{eine Einheit} definieren und verwalten können. Statt vier einzelner \texttt{docker run}-Befehle definieren wir alle Container in einer YAML-Datei.
Erstelle die Datei mit \texttt{nano /opt/fitness/docker-compose.yml}:
\begin{lstlisting}[language=YAML, caption={Vollständige docker-compose.yml}]
services:
# ============================================
# REVERSE PROXY (nimmt HTTP/HTTPS-Anfragen entgegen)
# ============================================
nginx-proxy:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
restart: unless-stopped
ports:
- ''80:80''
- ''443:443''
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- certs:/etc/nginx/certs
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
networks:
- proxy-net
# ============================================
# SSL-ZERTIFIKATE (automatisch via Let's Encrypt)
# ============================================
acme-companion:
image: nginxproxy/acme-companion
container_name: acme-companion
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- certs:/etc/nginx/certs
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- acme:/etc/acme.sh
environment:
- DEFAULT_EMAIL=robert@robre.de
- NGINX_PROXY_CONTAINER=nginx-proxy
depends_on:
- nginx-proxy
networks:
- proxy-net
# ============================================
# BACKEND (.NET 8 API)
# ============================================
backend:
image: fitness-api:latest
container_name: fitness-api
restart: unless-stopped
volumes:
- fitness-data:/app/data
networks:
- proxy-net
# ============================================
# FRONTEND (React + Nginx)
# ============================================
frontend:
image: fitness-web:latest
container_name: fitness-web
restart: unless-stopped
environment:
- VIRTUAL_HOST=robre.de,www.robre.de
- LETSENCRYPT_HOST=robre.de,www.robre.de
- VIRTUAL_PORT=80
depends_on:
- backend
networks:
- proxy-net
# ============================================
# VOLUMES (persistente Datenspeicher)
# ============================================
volumes:
certs:
vhost:
html:
acme:
fitness-data:
# ============================================
# NETZWERKE
# ============================================
networks:
proxy-net:
driver: bridge
\end{lstlisting}
\textbf{Zeile für Zeile erklärt:}
\paragraph{nginx-proxy}
\begin{itemize}
\item \texttt{image: nginxproxy/nginx-proxy} Offizielles Image des nginx-proxy-Projekts.
\item \texttt{ports: ''80:80'' / ''443:443''} Nur dieser Container lauscht auf den Standard-Webports. Alle HTTP/HTTPS-Anfragen kommen hier an.
\item \texttt{/var/run/docker.sock:/tmp/docker.sock:ro} Bindet den Docker-Socket ein (read-only). Darüber erkennt nginx-proxy, welche Container gerade laufen und welche Domains sie bedienen.
\item \texttt{certs:/etc/nginx/certs} Hier speichert der acme-companion die SSL-Zertifikate.
\end{itemize}
\paragraph{acme-companion}
\begin{itemize}
\item \texttt{DEFAULT\_EMAIL} Für Let's-Encrypt-Benachrichtigungen (z. B. wenn ein Zertifikat abläuft). \textbf{Muss eine echte, erreichbare E-Mail-Adresse sein!}
\item \texttt{NGINX\_PROXY\_CONTAINER=nginx-proxy} Sagt dem acme-companion explizit, wie der Proxy-Container heißt. Notwendig, wenn der Container-Name vom Standard abweicht.
\item \texttt{depends\_on: nginx-proxy} Stellt sicher, dass der Proxy zuerst startet.
\end{itemize}
\paragraph{frontend (entscheidende Umgebungsvariablen!)}
\begin{itemize}
\item \texttt{VIRTUAL\_HOST=robre.de,www.robre.de} Sagt nginx-proxy: ''Ich bin für diese Domains zuständig''. Alle Anfragen an \texttt{robre.de} oder \texttt{www.robre.de} werden an diesen Container weitergeleitet.
\item \texttt{LETSENCRYPT\_HOST=robre.de,www.robre.de} Sagt acme-companion: ''Hol SSL-Zertifikate für diese Domains''.
\item \texttt{VIRTUAL\_PORT=80} Der interne Port, auf dem der Container lauscht (unser Nginx im Frontend-Container).
\end{itemize}
\paragraph{Docker Volumes}
\begin{itemize}
\item \texttt{certs, vhost, html, acme} Speichern SSL-Zertifikate, Konfiguration und Challenge-Dateien. Diese Volumes werden von nginx-proxy und acme-companion geteilt.
\item \texttt{fitness-data} Persistente Speicherung der SQLite-Datenbank (siehe Schritt 03).
\end{itemize}
\subsubsection{Schritt 4: Container starten}
\begin{lstlisting}[language=Bash, caption={Alle Container im Hintergrund starten}]
cd /opt/fitness
docker compose up -d
\end{lstlisting}
\texttt{-d} steht für \textbf{detached} die Container laufen im Hintergrund und blockieren nicht das Terminal.
\subsubsection{Schritt 5: Status prüfen}
\begin{lstlisting}[language=Bash, caption={Laufende Container anzeigen}]
docker ps
\end{lstlisting}
Es sollten vier Container erscheinen: \texttt{nginx-proxy}, \texttt{acme-companion}, \texttt{fitness-api}, \texttt{fitness-web}.
\subsubsection{Schritt 6: Logs des acme-companion prüfen}
\begin{lstlisting}[language=Bash, caption={SSL-Zertifikatserstellung verfolgen}]
docker logs -f acme-companion
\end{lstlisting}
Bei erfolgreicher Zertifikatserstellung erscheinen diese Meldungen:
\begin{lstlisting}[language=Bash, caption={Erfolgreiche Ausgabe}]
[Thu May 7 09:56:11 UTC 2026] Registered
[Thu May 7 09:56:14 UTC 2026] Account update success
Creating/renewal robre.de certificates... (robre.de www.robre.de)
\end{lstlisting}
\subsection{Häufige Fehler und ihre Behebung}
\subsubsection{Fehler 1: ''can't get nginx-proxy container ID''}
\textbf{Ursache:} Der acme-companion findet den Proxy-Container nicht, weil dieser einen individuellen Namen hat (\texttt{nginx-proxy} statt dem Standard).
\textbf{Lösung:} Die Umgebungsvariable \texttt{NGINX\_PROXY\_CONTAINER=nginx-proxy} im acme-companion setzen (wie oben bereits eingebaut).
\subsubsection{Fehler 2: ''contact email has forbidden domain''}
\textbf{Ursache:} Die \texttt{DEFAULT\_EMAIL} enthält eine ungültige Domain wie \texttt{@example.com} Let's Encrypt lehnt reservierte Domains ab.
\textbf{Lösung:} Eine echte, erreichbare E-Mail-Adresse verwenden (z. B. \texttt{robert@robre.de}).
\subsubsection{Fehler 3: ''DNS problem: NXDOMAIN''}
\textbf{Ursache:} Die DNS-Propagation ist noch nicht abgeschlossen. Let's Encrypt kann die Domain nicht auflösen und verweigert die Zertifikatsausstellung.
\textbf{Lösung:} Warten, bis die Domain weltweit erreichbar ist (bis zu 24 Stunden). Dann den acme-companion neustarten:
\begin{lstlisting}[language=Bash, caption={Zertifikatserstellung erneut anstoßen}]
docker restart acme-companion
\end{lstlisting}
\subsection{Wie füge ich später weitere Subdomains hinzu?}
Das System ist extrem flexibel. Angenommen, du willst später eine Todo-App unter \texttt{todo.robre.de} hosten:
\begin{enumerate}
\item \textbf{DNS-Eintrag beim Hoster:} Einen neuen A-Record für \texttt{todo.robre.de} anlegen, der auf dieselbe Server-IP \texttt{185.209.229.167} zeigt. Oder: Der Wildcard-Eintrag \texttt{*.robre.de} fängt automatisch alle nicht explizit definierten Subdomains ab!
\item \textbf{Docker-Container:} Einen neuen Container mit den Umgebungsvariablen \texttt{VIRTUAL\_HOST=todo.robre.de} und \texttt{LETSENCRYPT\_HOST=todo.robre.de} starten.
\item \textbf{Fertig!} nginx-proxy erkennt den neuen Container automatisch und leitet Anfragen weiter. acme-companion holt automatisch ein SSL-Zertifikat.
\end{enumerate}
\textbf{Du musst auf dem Server nichts weiter konfigurieren alles läuft automatisch!}
\subsection{Wie funktioniert der Reverse Proxy im Detail?}
Wenn eine Anfrage an \texttt{https://robre.de/api/workouts} eingeht:
\begin{enumerate}
\item Die Anfrage kommt an Port 443 des Servers an (HTTPS).
\item nginx-proxy nimmt die Anfrage entgegen und entschlüsselt sie mit dem SSL-Zertifikat.
\item nginx-proxy schaut in den \texttt{Host}-Header: \texttt{robre.de}.
\item In seiner Konfiguration findet er: \texttt{robre.de} → Container \texttt{fitness-web}, Port 80.
\item Er leitet die Anfrage an \texttt{fitness-web:80} weiter (internes Docker-Netzwerk).
\item Der Nginx im Frontend-Container empfängt die Anfrage an \texttt{/api/workouts}.
\item Seine Konfiguration sagt: Alles mit \texttt{/api/} → weiterleiten an \texttt{fitness-api:5000}.
\item Das Backend verarbeitet die Anfrage und schickt die Antwort zurück durch die gesamte Kette.
\end{enumerate}
\textbf{Die gesamte Kommunikation innerhalb des Docker-Netzwerks (Schritte 5-8) läuft unverschlüsselt das ist okay, weil sie den Server nie verlässt.}
\subsection{Zusammenfassung}
Nach diesem Schritt haben wir:
\begin{itemize}
\item Einen automatischen Reverse Proxy (nginx-proxy), der Anfragen an die richtigen Container verteilt
\item Kostenlose SSL-Zertifikate von Let's Encrypt, die sich automatisch erneuern
\item Eine Infrastruktur, die beliebig viele weitere Domains und Subdomains aufnehmen kann ohne manuelle Konfiguration
\item Die App ist unter \texttt{https://robre.de} verschlüsselt erreichbar
\item Alle Voraussetzungen für die PWA-Installation sind erfüllt
\end{itemize}
\textbf{Wartezeit beachten:} Nach der Einrichtung kann es bis zu 24 Stunden dauern, bis die DNS-Propagation abgeschlossen ist und Let's Encrypt die Zertifikate ausstellen kann. Der acme-companion versucht es automatisch jede Stunde erneut.