21 KiB
Dokumentation – Pastebin-Service
Thema 1 – Custom Website Webarchitektur Projekt
Inhaltsverzeichnis
- Beschreibung der Umsetzung
- Architekturdiagramm
- Funktionsbeschreibung der Komponenten
- Installationsanleitung
- Konfigurationsanpassungen
- Post-Mortem-Log
- Screenshots
1. Beschreibung der Umsetzung
Das Projekt ist ein einfacher Pastebin-Service, der es Nutzern ermöglicht, Text online zu speichern und über einen Link zu teilen. Die Anwendung besteht aus vier Docker-Containern, die über Docker Compose orchestriert werden.
Kernfunktionalität
- Paste erstellen: Jeder Nutzer kann ohne Authentifizierung einen Paste erstellen (max. 100.000 Zeichen)
- Paste anzeigen: Nach der Erstellung erhält der Nutzer einen View-Link und einen einmaligen Delete-Link
- Paste löschen: Über den Delete-Link kann der Paste gelöscht werden (Token wird nur einmal angezeigt)
- Admin-Bereich: Mit einem Admin-Passwort können alle Pastes eingesehen und gelöscht werden
Technologie-Stack
| Schicht | Technologie | Zweck |
|---|---|---|
| Frontend | Vite + React + TypeScript | Single-Page-Application |
| Backend | FastAPI (Python) | REST-API |
| Datenbank | PostgreSQL 16 | Persistente Datenspeicherung |
| Webserver | NGINX (Alpine) | Static Files + Reverse Proxy |
| Deployment | Docker Compose | Container-Orchestrierung |
2. Architekturdiagramm
┌─────────────────────────────────────────────────────────────────┐
│ Internet │
│ Port 80 (HTTP) / 443 (HTTPS) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ NGINX Reverse Proxy │
│ (Container: proxy) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Port 80: 301 Redirect → HTTPS │ │
│ │ Port 443: TLS-Terminierung (cert.pem / key.pem) │ │
│ │ Security-Header: HSTS, X-Frame-Options, CSP, etc. │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ /api/* ──────────► http://backend:8000 │
│ /* ──────────► http://frontend:80 │
└────────────────────────────┬────────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ PostgreSQL │
│ (NGINX) │ │ (FastAPI) │ │ (Datenbank) │
│ Port 80 │ │ Port 8000 │ │ Port 5432 │
│ │ │ │ │ │
│ React SPA │ │ REST-API │ │ Pastes-Tabelle │
│ Static Files │ │ Uvicorn │ │ UUID PK/FK │
└─────────────────┘ └──────┬───────┘ └─────────────────┘
│
▼
┌──────────────┐
│ PostgreSQL │
│ (Volume) │
│ pgdata │
└──────────────┘
Netzwerk-Isolation
- Nur der proxy Container bindet auf öffentliche Ports (80, 443)
- backend, frontend und db sind nur über das interne Docker-Netzwerk erreichbar
- Keine externen Port-Mappings für Backend und Datenbank
3. Funktionsbeschreibung der Komponenten
3.1 NGINX Reverse Proxy (nginx/nginx.conf)
Aufgabe: Terminiert die TLS-Verbindung und leitet Requests an die internen Services weiter.
TLS-Terminierung (nginx/nginx.conf:13-20):
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
Das Zertifikat wird manuell im ssl/ Verzeichnis bereitgestellt. Es unterstützt TLS 1.2 und 1.3.
HTTP → HTTPS Redirect (nginx/nginx.conf:1-4):
server {
listen 80;
server_name static.155.116.167.89.clients.your-server.de;
return 301 https://$host$request_uri;
}
Alle HTTP-Anfragen werden permanent auf HTTPS umgeleitet.
Reverse Proxy Routing (nginx/nginx.conf:30-46):
location /api/ {
proxy_pass http://backend:8000;
...
}
location / {
proxy_pass http://frontend:80;
...
}
Requests an /api/* werden an den FastAPI-Backend-Container weitergeleitet, alle anderen an den Frontend-NGINX.
Security-Hardening (nginx/nginx.conf:24-28):
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
| Header | Funktion |
|---|---|
Strict-Transport-Security (HSTS) |
Erzwingt HTTPS für 2 Jahre, inkl. Subdomains |
X-Frame-Options: DENY |
Verhindert Einbettung in iframes (Clickjacking-Schutz) |
X-Content-Type-Options: nosniff |
Verhindert MIME-Type-Sniffing |
Referrer-Policy |
Kontrolliert Referrer-Weitergabe |
Permissions-Policy |
Deaktiviert Kamera, Mikrofon, Geolocation |
Server-Header entfernen (nginx/nginx.conf:11, 36-37, 45):
server_tokens off; # Blendet NGINX-Version aus
proxy_hide_header Server; # Entfernt Server-Header aus Backend-Antworten
proxy_hide_header X-Powered-By; # Entfernt Technologie-Header
3.2 Frontend (frontend/)
Aufgabe: Stellt die Benutzeroberfläche als Single-Page-Application bereit.
Build-Prozess (frontend/Dockerfile:1-9):
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
Das Frontend wird mit Vite gebaut. Die entstehenden Static Files werden in den NGINX-Container kopiert.
Static File Serving (frontend/nginx.conf:8-10):
location / {
try_files $uri $uri/ /index.html;
}
Die try_files-Direktive ermöglicht Client-Side-Routing: Alle Pfade werden auf index.html umgeleitet, sodass React Router funktioniert.
SPA-Routing (frontend/src/main.tsx):
<Routes>
<Route path="/" element={<HomePage />} /> {/* Paste erstellen */}
<Route path="paste/:id" element={<PastePage />} /> {/* Paste anzeigen */}
<Route path="admin" element={<AdminPage />} /> {/* Admin-Bereich */}
</Routes>
API-Client (frontend/src/api/client.ts):
Der API-Client kapselt alle HTTP-Requests an das Backend in typisierte Funktionen. Er verwendet die nativ fetch-API und gibt die Antworten als TypeScript-Interfaces zurück:
const API_BASE = "/api"; // Relativer Pfad → wird vom NGINX Proxy weitergeleitet
| Funktion | HTTP-Methode | Endpoint | Beschreibung |
|---|---|---|---|
createPaste(content) |
POST | /api/pastes |
Erstellt einen neuen Paste |
getPaste(id) |
GET | /api/pastes/{id} |
Ruft einen Paste ab |
deletePaste(id, token) |
DELETE | /api/pastes/{id}?token=... |
Löscht einen Paste (mit Token) |
listPastes(adminPassword) |
GET | /api/admin/pastes |
Listet alle Pastes (Admin) |
adminDeletePaste(id, pw) |
DELETE | /api/admin/pastes/{id} |
Löscht einen Paste (Admin) |
Die Funktionen werden von den Page-Komponenten importiert und genutzt. Beispiel aus HomePage.tsx:
const result = await createPaste(content);
navigate(`/paste/${result.id}?token=${result.delete_token}`);
Die Admin-Funktionen senden das Passwort über den X-Admin-Password Header:
const res = await fetch(`${API_BASE}/admin/pastes`, {
headers: { "X-Admin-Password": adminPassword },
});
3.3 Backend (backend/)
Aufgabe: Stellt die REST-API bereit und kommuniziert mit der Datenbank.
API-Endpunkte (backend/app/main.py):
| Methode | Pfad | Beschreibung | Zeile |
|---|---|---|---|
POST |
/api/pastes |
Neuen Paste erstellen | 34 |
GET |
/api/pastes/{id} |
Paste abrufen | 45 |
DELETE |
/api/pastes/{id}?token=... |
Paste löschen (mit Token) | 53 |
GET |
/api/admin/pastes |
Alle Pastes auflisten (Admin) | 69 |
DELETE |
/api/admin/pastes/{id} |
Paste als Admin löschen | 78 |
Admin-Authentifizierung (backend/app/main.py:29-31):
def verify_admin(password: str = Header(..., alias="X-Admin-Password")):
if password != settings.admin_password:
raise HTTPException(status_code=401, detail="Invalid admin password")
Das Admin-Passwort wird über den X-Admin-Password Header übermittelt und gegen die Umgebungsvariable geprüft.
Datenbankmodell (backend/app/models.py:11-21):
class Paste(Base):
__tablename__ = "pastes"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
content: Mapped[str] = mapped_column(Text, nullable=False)
delete_token: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, default=uuid.uuid4)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
id: UUID als Primärschlüssel (verhindert Enumeration)delete_token: Separater UUID für das Löschen (wird nur bei Erstellung ausgegeben)content: Text mit max. 100.000 Zeichen
3.4 PostgreSQL Datenbank
Aufgabe: Persistente Speicherung der Pastes.
Konfiguration (docker-compose.yml:2-10):
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
Die Datenbank-Credentials werden über Umgebungsvariablen aus .env geladen. Die Daten werden in einem Docker-Volume (pgdata) persistent gespeichert.
Port-Binding: Der PostgreSQL-Container hat kein ports:-Mapping und ist daher nur aus dem internen Docker-Netzwerk erreichbar.
4. Installationsanleitung
4.1 Voraussetzungen
- Docker und Docker Compose installiert
- Domain/Server-IP verfügbar:
static.155.116.167.89.clients.your-server.de - SSL-Zertifikat vorhanden
4.2 Projekt klonieren
git clone <repository-url>
cd Web-Architekturen-Projekt
4.3 SSL-Zertifikat bereitstellen
Zertifikat im ssl/ Verzeichnis ablegen:
# Bei vorhandenem Let's Encrypt Zertifikat:
cp /etc/letsencrypt/live/static.155.116.167.89.clients.your-server.de/fullchain.pem ssl/cert.pem
cp /etc/letsencrypt/live/static.155.116.167.89.clients.your-server.de/privkey.pem ssl/key.pem
# Oder Self-Signed für Tests:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl/key.pem -out ssl/cert.pem \
-subj "/CN=static.155.116.167.89.clients.your-server.de"
4.4 Umgebungsvariablen konfigurieren
.env Datei anpassen:
ADMIN_PASSWORD=sicheres-passwort-hier
POSTGRES_USER=pastebin
POSTGRES_PASSWORD=datenbank-passwort-hier
POSTGRES_DB=pastebin
4.5 Container starten
docker compose up --build -d
4.6 Überprüfung
# Container-Status prüfen
docker compose ps
# Logs prüfen
docker compose logs
# Einzelne Services
docker compose logs proxy
docker compose logs backend
docker compose logs db
Die Anwendung ist erreichbar unter: https://static.155.116.167.89.clients.your-server.de
4.7 Neustart
docker compose down
docker compose up -d
Die Datenbank-Daten bleiben durch das Docker-Volume erhalten.
5. Konfigurationsanpassungen
5.1 NGINX Reverse Proxy (nginx/nginx.conf)
Alle Konfigurationen weichen von der Standardkonfiguration ab:
| Zeile | Konfiguration | Beschreibung |
|---|---|---|
| 1-4 | server { listen 80; return 301 ... } |
HTTP → HTTPS Redirect |
| 11 | server_tokens off; |
NGINX-Version ausblenden |
| 13-14 | ssl_certificate / ssl_certificate_key |
Manuelle TLS-Zertifikate |
| 16-20 | ssl_protocols / ssl_ciphers |
TLS 1.2/1.3, sichere Ciphers |
| 24-28 | add_header ... |
5 Security-Header |
| 31 | proxy_pass http://backend:8000; |
API-Requests an Backend |
| 33-35 | proxy_set_header ... |
Client-IP und Proto weiterleiten |
| 36-37 | proxy_hide_header ... |
Server-Header entfernen |
| 41 | proxy_pass http://frontend:80; |
Frontend-Requests an NGINX |
5.2 Frontend-NGINX (frontend/nginx.conf)
| Zeile | Konfiguration | Beschreibung |
|---|---|---|
| 9 | try_files $uri $uri/ /index.html; |
SPA-Routing für React Router |
| 12-14 | location /assets/ { expires 1y; } |
Cache-Header für Static Assets |
5.3 Docker Compose (docker-compose.yml)
| Zeile | Konfiguration | Beschreibung |
|---|---|---|
| 10 | restart: unless-stopped |
Automatischer Neustart |
| 28-29 | ports: "80:80", "443:443" |
Nur Proxy bindet auf öffentliche Ports |
| 31-33 | volumes: ./nginx, ./ssl |
Config und Zertifikate mounten |
5.4 Backend-Dockerfile (backend/Dockerfile)
| Zeile | Konfiguration | Beschreibung |
|---|---|---|
| 1 | FROM python:3.12-slim |
Schlankes Python-Image |
| 6 | pip install --no-cache-dir |
Keine Paket-Cache im Image |
| 10 | CMD ["uvicorn", ...] |
FastAPI mit Uvicorn starten |
5.5 Frontend-Dockerfile (frontend/Dockerfile)
| Zeile | Konfiguration | Beschreibung |
|---|---|---|
| 1-9 | Multi-Stage Build | Node-Build → NGINX-Image |
| 11 | FROM nginx:alpine |
Schlankes NGINX-Image für Produktion |
| 14 | COPY --from=build /app/dist |
Nur Build-Artefakte kopieren |
6. Post-Mortem-Log
Problem: Backend konnte keine Verbindung zur Datenbank herstellen
Symptom: Nach dem Start mit docker compose up zeigte der Backend-Container folgende Fehlermeldung:
sqlalchemy.exc.OperationalError: (asyncpg.exceptions.ConnectionDoesNotExistError)
connection to server at "db", port 5432 failed
Analyse:
-
Container-Status geprüft:
docker compose psDer
db-Container war im Status "starting", derbackend-Container bereits im Status "running". -
Logs der Datenbank geprüft:
docker compose logs dbDie Datenbank war noch dabei, die Initialisierung durchzuführen.
-
Ursache identifiziert: Der
depends_onEintrag imdocker-compose.ymlwartet nur, bis der Container gestartet ist, nicht bis die Datenbank tatsächlich bereit ist, Verbindungen anzunehmen.
Lösung:
Die lifespan-Funktion in backend/app/main.py wurde implementiert, die beim Start der Anwendung die Datenbank-Tabellen erstellt:
@asynccontextmanager
async def lifespan(app: FastAPI):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
Zusätzlich wurde ein restart: unless-stopped Policy für alle Container konfiguriert, sodass der Backend-Container bei einem fehlgeschlagenen Start automatisch neu gestartet wird und es erneut versucht, sobald die Datenbank bereit ist.
Verifikation:
docker compose logs backend
# Erfolgreiche Ausgabe: "INFO: Uvicorn running on http://0.0.0.0:8000"
7. Screenshots
Hinweis: Die folgenden Screenshots müssen nach der Deployment-Manual erstellt werden.
7.1 Container-Status
docker compose ps
Erwartete Ausgabe:
NAME STATUS PORTS
proxy Up 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
frontend Up 80/tcp
backend Up 8000/tcp
db Up 5432/tcp
7.2 HTTPS-Verbindung testen
curl -I https://static.155.116.167.89.clients.your-server.de
Erwartete Ausgabe (Auszug):
HTTP/2 200
strict-transport-security: max-age=63072000; includeSubDomains
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), camera=(), microphone=()
7.3 HTTP → HTTPS Redirect testen
curl -I http://static.155.116.167.89.clients.your-server.de
Erwartete Ausgabe:
HTTP/1.1 301 Moved Permanently
Location: https://static.155.116.167.89.clients.your-server.de/
7.4 API-Endpoint testen
# Paste erstellen
curl -X POST https://static.155.116.167.89.clients.your-server.de/api/pastes \
-H "Content-Type: application/json" \
-d '{"content": "Test Paste"}'
Erwartete Ausgabe:
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"delete_token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"created_at": "2026-01-01T12:00:00Z"
}
7.5 Paste abrufen
# Paste anzeigen (ID aus vorheriger Antwort einsetzen)
curl https://static.155.116.167.89.clients.your-server.de/api/pastes/<ID>
7.6 Admin-Authentifizierung testen
# Ohne Passwort (sollte 401 liefern)
curl https://static.155.116.167.89.clients.your-server.de/api/admin/pastes
# Mit Passwort
curl -H "X-Admin-Password: <ADMIN_PASSWORD>" \
https://static.155.116.167.89.clients.your-server.de/api/admin/pastes
7.7 Server-Header prüfen
curl -sI https://static.155.116.167.89.clients.your-server.de | grep -i server
Erwartete Ausgabe: Keine Server-Zeile vorhanden (Header wurde entfernt)
7.8 Frontend-Oberfläche
Screenshot der Startseite unter https://static.155.116.167.89.clients.your-server.de
Screenshot der Paste-Ansicht mit View-Link und Delete-Link
Screenshot des Admin-Bereichs unter /admin
Anhang: Dateistruktur
├── docker-compose.yml # Container-Orchestrierung
├── .env # Umgebungsvariablen (nicht im Repo)
├── .gitignore
├── dokumentation.md # Diese Dokumentation
├── nginx/
│ └── nginx.conf # Reverse-Proxy-Konfiguration
├── ssl/
│ ├── cert.pem # Öffentliches Zertifikat
│ ├── key.pem # Privater Schlüssel
│ └── README.md # Anleitung für Zertifikat
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/
│ ├── main.py # FastAPI-Anwendung
│ ├── models.py # SQLAlchemy-Modelle
│ ├── schemas.py # Pydantic-Schemas
│ ├── database.py # Datenbankverbindung
│ └── config.py # Konfiguration
└── frontend/
├── Dockerfile
├── nginx.conf # Frontend-NGINX-Konfiguration
├── package.json
├── vite.config.ts
├── tsconfig.json
├── index.html
└── src/
├── main.tsx # React-Einstiegspunkt
├── App.tsx # Layout
├── api/client.ts # API-Funktionen
└── pages/
├── HomePage.tsx # Paste erstellen
├── PastePage.tsx # Paste anzeigen
└── AdminPage.tsx # Admin-Bereich