Files

21 KiB
Raw Permalink Blame History

Dokumentation Pastebin-Service

Thema 1 Custom Website Webarchitektur Projekt


Inhaltsverzeichnis

  1. Beschreibung der Umsetzung
  2. Architekturdiagramm
  3. Funktionsbeschreibung der Komponenten
  4. Installationsanleitung
  5. Konfigurationsanpassungen
  6. Post-Mortem-Log
  7. 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:

  1. Container-Status geprüft:

    docker compose ps
    

    Der db-Container war im Status "starting", der backend-Container bereits im Status "running".

  2. Logs der Datenbank geprüft:

    docker compose logs db
    

    Die Datenbank war noch dabei, die Initialisierung durchzuführen.

  3. Ursache identifiziert: Der depends_on Eintrag im docker-compose.yml wartet 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