Zum Inhalt springen
Web & Code

Komplette Anleitung: FastAPI, PostgreSQL, Nginx und SSL auf Ubuntu 22.04 für mehrere Subdomains

In dieser sehr ausführlichen kompletten Schritt-für-Schritt-Anleitung lernst du, wie du drei unabhängige FastAPI-Anwendungen auf einem Ubuntu 22.04-Server einrichtest und betreibst. Jede Anwendung läuft unter einer eigenen Subdomain, speichert Daten in PostgreSQL, verwendet…

Von Sonoya Redaktion 17 Min. Lesezeit
Komplette Anleitung: FastAPI, PostgreSQL, Nginx und SSL auf Ubuntu 22.04 für mehrere Subdomains
Komplette Anleitung: FastAPI, PostgreSQL, Nginx und SSL auf Ubuntu 22.04 für mehrere Subdomains

In dieser sehr ausführlichen kompletten Schritt-für-Schritt-Anleitung lernst du, wie du drei unabhängige FastAPI-Anwendungen auf einem Ubuntu 22.04-Server einrichtest und betreibst. Jede Anwendung läuft unter einer eigenen Subdomain, speichert Daten in PostgreSQL, verwendet Nginx als Reverse Proxy und ist per Let’s Encrypt SSL-Zertifikat gesichert.

Wir zeigen dir, wie du Benutzerregistrierung, JWT-Authentifizierung, vorbefüllte Formulare, Summenberechnung und individuelles Logging für jede App integrierst. Zusätzlich behandeln wir die Einrichtung von Umgebungsvariablen, Logging-Mechanismen und Best Practices für eine skalierbare und sichere Architektur. Diese Anleitung richtet sich an Entwickler, die eine leistungsfähige, wartbare und leicht erweiterbare Webarchitektur aufbauen möchten.

Ziel: Eine Umgebung schaffen, in der du drei getrennte FastAPI-Anwendungen auf einem Ubuntu 22.04 Server unter drei verschiedenen Subdomains betreibst, jede mit einem eigenen Funktionsumfang (z. B. unterschiedlicher Funktionsfokus oder Mandanten). Jede Anwendung verfügt über Benutzeranmeldung, Datenerfassung per Formular, Speicherung in einer PostgreSQL-Datenbank, Vorbefüllung alter Daten, Summenberechnung, ein Logging-Konzept sowie sichere SSL-Verschlüsselung und Routing durch Nginx. Zusätzlich bekommst du Methoden an die Hand, um spätere Erweiterungen, Skalierungen und funktionale Ausbauten (wie Einbindung weiterer APIs, mehr Felder, komplexere Business-Logik etc.) problemlos durchzuführen.


Entschuldige bitte das Missverständnis in meiner vorherigen Antwort. Hier ist eine vollständig überarbeitete und umfassende Schritt-für-Schritt-Anleitung, die alle bisherigen Anforderungen integriert. Diese Anleitung führt dich von der Grundinstallation bis zur vollständigen Konfiguration von drei FastAPI-Anwendungen auf unterschiedlichen Subdomains mit Benutzeranmeldung, Formularverwaltung, Datenbankintegration, Logging und SSL-Verschlüsselung.


Inhaltsverzeichnis

  1. Server- und DNS-Grundlagen

  2. System-Setup auf Ubuntu 22.04

  3. Installation und Konfiguration von PostgreSQL

  4. Projektstruktur, virtuelle Umgebungen und Basis-Setup

  5. Erstellung der FastAPI-Anwendungen (App1, App2, App3)

  6. Benutzerregistrierung, Authentifizierung (JWT) und Passwort-Hashing

  7. Datenbankmodelle, Formular-Daten, Vorbefüllung und Summenberechnung

  8. Vorlagen (Templates) und Formulare mit Jinja2

  9. Drei FastAPI-Apps auf unterschiedlichen Subdomains (Nginx Reverse Proxy)

  10. Logging-Konzept für jede Anwendung (Backend- und Nginx-Logging)

  11. SSL-Integration mit Let’s Encrypt

  12. Beispiele, Testdaten und Prüfung der Funktionsweise

  13. Skalierung, Erweiterung und Best Practices

  14. Zusammenfassung


1. Server- und DNS-Grundlagen

Domain- und DNS-Konfiguration

Bevor du mit der Einrichtung deines Servers beginnst, stelle sicher, dass du eine Domain besitzt und Zugriff auf die DNS-Einstellungen hast.

  1. Subdomains erstellen: Erstelle drei Subdomains, z. B.:

    • app1.deinedomain.com

    • app2.deinedomain.com

    • app3.deinedomain.com

  2. DNS-Einträge hinzufügen: Füge für jede Subdomain einen A-Record hinzu, der auf die IP-Adresse deines Ubuntu-Servers zeigt. Beispiel: Subdomain Typ Wert app1 A Deine_Server_IP app2 A Deine_Server_IP app3 A Deine_Server_IP

  3. DNS-Einträge überprüfen: Warte einige Minuten bis Stunden, bis die DNS-Einträge propagiert sind. Überprüfe die Einträge mit: dig app1.deinedomain.com dig app2.deinedomain.com dig app3.deinedomain.com Wenn die IP-Adresse deines Servers zurückgegeben wird, ist alles korrekt.


2. System-Setup auf Ubuntu 22.04

Führe die folgenden Schritte auf deinem Ubuntu 22.04 Server als Standardbenutzer mit sudo-Rechten aus.

  1. System aktualisieren: `sudo apt update && sudo apt upgrade -y`

  2. Benötigte Pakete installieren:sudo apt install python3-pip python3-venv git curl nginx certbot python3-certbot-nginx postgresql postgresql-contrib -y

    • Python3 & venv: Zum Betrieb deiner Python-Anwendungen.

    • Nginx: Als Reverse Proxy und Webserver.

    • Certbot: Für Let’s Encrypt SSL-Zertifikate.

    • PostgreSQL: Für die Datenbank.

  3. Firewall konfigurieren (optional, aber empfohlen): Wenn UFW (Uncomplicated Firewall) installiert ist, konfiguriere die notwendigen Regeln. sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable Überprüfe den Status: sudo ufw status


3. Installation und Konfiguration von PostgreSQL

  1. PostgreSQL-Dienst starten und aktivieren: sudo systemctl start postgresql sudo systemctl enable postgresql

  2. PostgreSQL-Benutzer und Datenbank erstellen: Wechsel zum PostgreSQL-Benutzer und öffne die PostgreSQL-Shell: sudo -i -u postgres psql In der psql-Konsole: CREATE DATABASE meine_datenbank; CREATE USER mein_user WITH PASSWORD 'starkes_passwort'; GRANT ALL PRIVILEGES ON DATABASE meine_datenbank TO mein_user; \q exit Wichtig: Ersetze meine_datenbank, mein_user und starkes_passwort mit deinen eigenen Werten.

  3. Verbindungsdetails notieren: Diese Werte werden später in den FastAPI-Anwendungen benötigt.


4. Projektstruktur, virtuelle Umgebungen und Basis-Setup

  1. Projektverzeichnis erstellen: mkdir -p ~/fastapi_projekte/{app1,app2,app3}/app mkdir ~/fastapi_projekte/logs

  2. Virtuelle Umgebungen einrichten und Abhängigkeiten installieren: Für jede App (app1, app2, app3) führst du ähnliche Schritte aus. Hier wird app1 als Beispiel gezeigt. cd ~/fastapi_projekte/app1 python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install fastapi uvicorn[standard] sqlalchemy psycopg2-binary alembic jinja2 python-multipart passlib[bcrypt] python-jose[cryptography] gunicorn python-dotenv

    • fastapi, uvicorn: Basis Webframework und ASGI-Server.

    • sqlalchemy, psycopg2-binary: Datenbankanbindung.

    • alembic: Datenbankmigrationen.

    • jinja2: Templates für Formulare.

    • python-multipart: Formulardaten-Handling.

    • passlib[bcrypt]: Passwort-Hashing.

    • python-jose[cryptography]: JWT-Token-Verarbeitung.

    • gunicorn: Produktionsfähiger Webserver.

    • python-dotenv: Zum Laden von Umgebungsvariablen aus .env-Dateien.

  3. Wiederhole die Schritte für app2 und app3: # Für app2 cd ~/fastapi_projekte/app2 python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install fastapi uvicorn[standard] sqlalchemy psycopg2-binary alembic jinja2 python-multipart passlib[bcrypt] python-jose[cryptography] gunicorn python-dotenv # Für app3 cd ~/fastapi_projekte/app3 python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install fastapi uvicorn[standard] sqlalchemy psycopg2-binary alembic jinja2 python-multipart passlib[bcrypt] python-jose[cryptography] gunicorn python-dotenv

  4. Log-Verzeichnis sicherstellen: Stelle sicher, dass das Log-Verzeichnis existiert und Schreibrechte hat. mkdir -p ~/fastapi_projekte/logs sudo chown -R $USER:www-data ~/fastapi_projekte/logs sudo chmod -R 775 ~/fastapi_projekte/logs


5. Erstellung der FastAPI-Anwendungen (App1, App2, App3)

Wir zeigen hier die vollständige Erstellung von app1. Die Schritte für app2 und app3 sind analog, du kannst jedoch je nach Bedarf unterschiedliche Funktionalitäten implementieren.

5.1 Verzeichnisstruktur von App1

~/fastapi_projekte/app1/
??? app/
?   ??? __init__.py
?   ??? main.py
?   ??? models.py
?   ??? schemas.py
?   ??? database.py
?   ??? auth.py
?   ??? logging_config.py
?   ??? templates/
?   ?   ??? form.html
?   ??? static/
?       ??? style.css
??? venv/
??? start.sh
??? alembic.ini
??? alembic/
    ??? versions/

5.2 Dateien erstellen

  1. database.py: # ~/fastapi_projekte/app1/app/database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base import os from dotenv import load_dotenv load_dotenv() DATABASE_URL = os.getenv("DATABASE_URL") engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base()

  2. models.py: # ~/fastapi_projekte/app1/app/models.py from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, JSON from sqlalchemy.orm import relationship from .database import Base import datetime class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) entries = relationship("Entry", back_populates="owner") class Entry(Base): __tablename__ = "entries" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey('users.id')) data = Column(JSON, nullable=False) timestamp = Column(DateTime, default=datetime.datetime.utcnow) total = Column(Integer, default=0) owner = relationship("User", back_populates="entries")

  3. schemas.py: # ~/fastapi_projekte/app1/app/schemas.py from pydantic import BaseModel from typing import Dict import datetime class UserCreate(BaseModel): username: str password: str class UserLogin(BaseModel): username: str password: str class EntryCreate(BaseModel): data: Dict[str, int] class EntryOut(BaseModel): id: int data: Dict[str, int] timestamp: datetime.datetime total: int class Config: orm_mode = True class Token(BaseModel): access_token: str token_type: str

  4. auth.py: # ~/fastapi_projekte/app1/app/auth.py from passlib.context import CryptContext from jose import jwt from datetime import datetime, timedelta from sqlalchemy.orm import Session from . import models from fastapi import HTTPException, status import os from dotenv import load_dotenv load_dotenv() SECRET_KEY = os.getenv("SECRET_KEY") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def get_password_hash(password): return pwd_context.hash(password) def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def create_access_token(data: dict, expires_delta: timedelta = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def authenticate_user(db: Session, username: str, password: str): user = db.query(models.User).filter(models.User.username == username).first() if not user or not verify_password(password, user.hashed_password): return False return user

  5. logging_config.py: # ~/fastapi_projekte/app1/app/logging_config.py import logging from logging.handlers import RotatingFileHandler import os def setup_logging(): logger = logging.getLogger("app1") logger.setLevel(logging.INFO) # Console Handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # File Handler log_file = os.path.expanduser("~/fastapi_projekte/logs/app1.log") file_handler = RotatingFileHandler(log_file, maxBytes=1000000, backupCount=5) file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) return logger

  6. main.py: # ~/fastapi_projekte/app1/app/main.py from fastapi import FastAPI, Depends, HTTPException, status, Request, Form from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from . import models, schemas, auth from .database import SessionLocal, engine from .logging_config import setup_logging from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse from jose import JWTError, jwt import os from dotenv import load_dotenv load_dotenv() models.Base.metadata.create_all(bind=engine) app = FastAPI() logger = setup_logging() templates = Jinja2Templates(directory="app/templates") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Dependency zur Datenbank def get_db(): db = SessionLocal() try: yield db finally: db.close() # Authentifizierungsfunktion def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Können den Benutzer nicht validieren", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]) user_id: int = payload.get("id") if user_id is None: raise credentials_exception except JWTError: raise credentials_exception user = db.query(models.User).filter(models.User.id == user_id).first() if user is None: raise credentials_exception return user # Benutzerregistrierung @app.post("/register", response_model=schemas.Token) def register(user: schemas.UserCreate, db: Session = Depends(get_db)): db_user = db.query(models.User).filter(models.User.username == user.username).first() if db_user: logger.warning(f"Registrierungsversuch mit bestehendem Benutzernamen: {user.username}") raise HTTPException(status_code=400, detail="Benutzername bereits vergeben") hashed_password = auth.get_password_hash(user.password) new_user = models.User(username=user.username, hashed_password=hashed_password) db.add(new_user) db.commit() db.refresh(new_user) access_token = auth.create_access_token(data={"id": new_user.id}) logger.info(f"Neuer Benutzer registriert: {user.username}") return {"access_token": access_token, "token_type": "bearer"} # Benutzeranmeldung @app.post("/token", response_model=schemas.Token) def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = auth.authenticate_user(db, form_data.username, form_data.password) if not user: logger.warning(f"Ungültige Anmeldeversuche für Benutzer: {form_data.username}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Ungültige Anmeldedaten", headers={"WWW-Authenticate": "Bearer"}, ) access_token = auth.create_access_token(data={"id": user.id}) logger.info(f"Benutzer angemeldet: {form_data.username}") return {"access_token": access_token, "token_type": "bearer"} # Formularseite anzeigen @app.get("/", response_class=HTMLResponse) def read_form(request: Request, token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): user = get_current_user(token, db) latest_entry = db.query(models.Entry).filter(models.Entry.user_id == user.id).order_by(models.Entry.timestamp.desc()).first() data = latest_entry.data if latest_entry else {} total = latest_entry.total if latest_entry else 0 logger.info(f"Formularseite aufgerufen von Benutzer: {user.username}") return templates.TemplateResponse("form.html", {"request": request, "data": data, "total": total}) # Formular absenden @app.post("/submit", response_class=HTMLResponse) def submit_form(request: Request, feld1: int = Form(...), feld2: int = Form(...), token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): user = get_current_user(token, db) data = {"feld1": feld1, "feld2": feld2} total = feld1 + feld2 new_entry = models.Entry(user_id=user.id, data=data, total=total) db.add(new_entry) db.commit() db.refresh(new_entry) logger.info(f"Neuer Eintrag von Benutzer {user.username}: {data} mit Summe {total}") return templates.TemplateResponse("form.html", {"request": request, "data": data, "total": total}) # Einträge anzeigen @app.get("/entries", response_model=list[schemas.EntryOut]) def get_entries(db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)): entries = db.query(models.Entry).filter(models.Entry.user_id == current_user.id).order_by(models.Entry.timestamp.desc()).all() logger.info(f"Einträge abgerufen für Benutzer: {current_user.username}") return entries # Eintrag bearbeiten @app.post("/entries/{entry_id}", response_model=schemas.EntryOut) def update_entry(entry_id: int, feld1: int = Form(...), feld2: int = Form(...), db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)): entry = db.query(models.Entry).filter(models.Entry.id == entry_id, models.Entry.user_id == current_user.id).first() if not entry: logger.warning(f"Eintrag nicht gefunden: ID {entry_id} für Benutzer {current_user.username}") raise HTTPException(status_code=404, detail="Eintrag nicht gefunden") entry.data = {"feld1": feld1, "feld2": feld2} entry.total = feld1 + feld2 db.commit() db.refresh(entry) logger.info(f"Eintrag aktualisiert: ID {entry_id} für Benutzer {current_user.username}") return entry

  7. .env Datei erstellen: Erstelle eine .env-Datei im Wurzelverzeichnis von app1: touch ~/fastapi_projekte/app1/.env Füge die folgenden Zeilen hinzu: SECRET_KEY=dein_geheimer_schluessel DATABASE_URL=postgresql://mein_user:starkes_passwort@localhost/meine_datenbank Wichtig: Ersetze dein_geheimer_schluessel, mein_user, starkes_passwort und meine_datenbank mit deinen eigenen Werten.

  8. alembic konfigurieren (optional für Migrationen): Initialisiere Alembic für Datenbankmigrationen. cd ~/fastapi_projekte/app1 alembic init alembic Bearbeite alembic.ini und setze sqlalchemy.url auf deine DATABASE_URL aus .env. # alembic.ini sqlalchemy.url = postgresql://mein_user:starkes_passwort@localhost/meine_datenbank Bearbeite alembic/env.py, um die Modelle zu importieren: # alembic/env.py import sys import os from logging.config import fileConfig from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context # Füge den Pfad zur App hinzu sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from app.database import Base from app import models # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) target_metadata = Base.metadata def run_migrations_offline(): """Run migrations in 'offline' mode.""" url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"} ) with context.begin_transaction(): context.run_migrations() def run_migrations_online(): """Run migrations in 'online' mode.""" connectable = engine_from_config( config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool, ) with connectable.connect() as connection: context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() Führe die Migration durch: alembic revision --autogenerate -m "Initial migration" alembic upgrade head

  9. start.sh erstellen: Erstelle ein Startskript, um die Anwendung mit Gunicorn zu starten. # ~/fastapi_projekte/app1/start.sh #!/bin/bash source ~/fastapi_projekte/app1/venv/bin/activate exec gunicorn -k uvicorn.workers.UvicornWorker app.main:app --bind 127.0.0.1:8001 --log-file ~/fastapi_projekte/logs/app1.log Mache das Skript ausführbar: chmod +x ~/fastapi_projekte/app1/start.sh

  10. templates/form.html erstellen: <!-- ~/fastapi_projekte/app1/app/templates/form.html --> <!DOCTYPE html> <html lang="de"> <head> <meta charset="UTF-8"> <title>Formular App1</title> <link rel="stylesheet" href="/static/style.css"> </head> <body> <h1>Formular</h1> <form action="/submit" method="post"> {% if data %} {% for key, value in data.items() %} <label for="{{ key }}">{{ key }}:</label> <input type="number" id="{{ key }}" name="{{ key }}" value="{{ value }}" required><br><br> {% endfor %} {% else %} <label for="feld1">Feld 1:</label> <input type="number" id="feld1" name="feld1" value="0" required><br><br> <label for="feld2">Feld 2:</label> <input type="number" id="feld2" name="feld2" value="0" required><br><br> {% endif %} <button type="submit">Absenden</button> </form> <h2>Gesamtsumme: {{ total }}</h2> </body> </html>

  11. static/style.css erstellen: /* ~/fastapi_projekte/app1/app/static/style.css */ body { font-family: Arial, sans-serif; margin: 20px; } label { display: inline-block; width: 100px; } input { padding: 5px; margin-bottom: 10px; } button { padding: 10px 20px; }

  12. Wiederhole die Schritte für app2 und app3: Passe die Dateien nach Bedarf an, z. B. unterschiedliche Portzuweisungen, unterschiedliche Subdomain-spezifische Inhalte.


6. Benutzerregistrierung, Authentifizierung (JWT) und Passwort-Hashing

Dieser Abschnitt ist bereits im Schritt 5.2 (Dateien erstellen) integriert, insbesondere in auth.py und main.py. Hier nochmal eine kurze Zusammenfassung:

  • auth.py enthält Funktionen zum Hashen von Passwörtern, Verifizieren von Passwörtern, Erstellen von JWT-Tokens und Authentifizieren von Benutzern.
  • main.py enthält Endpunkte für die Registrierung (/register), Anmeldung (/token), und geschützte Routen, die nur authentifizierten Benutzern zugänglich sind.

7. Datenbankmodelle, Formular-Daten, Vorbefüllung und Summenberechnung

Dieser Abschnitt ist ebenfalls im Schritt 5.2 abgedeckt. Hier nochmals die Kernpunkte:

  • models.py definiert die Datenbankmodelle für User und Entry.

  • schemas.py definiert Pydantic-Schemas für die Datenvalidierung.

  • Summenberechnung erfolgt beim Absenden des Formulars in main.py, indem die Werte der Formulareingaben summiert und in der Datenbank gespeichert werden.

Beispiel Summenberechnung:

def calculate_total(data: dict) -> int:
    return sum(data.values())

Diese Funktion wird in submit_form verwendet, um die Summe der eingegebenen Werte zu berechnen.


8. Vorlagen (Templates) und Formulare mit Jinja2

Die Templates wurden im Schritt 5.2 erstellt. Hier sind weitere Details:

  • form.html enthält das HTML-Formular, das entweder mit den letzten eingegebenen Daten vorbefüllt wird oder Standardwerte anzeigt, wenn keine Daten vorhanden sind.

Erweiterung für die Anzeige und Bearbeitung von Einträgen:

Falls du zusätzliche Funktionen wie das Bearbeiten früherer Einträge integrieren möchtest, kannst du das Template erweitern:

<!-- Erweiterte form.html -->
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Formular App1</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <h1>Formular</h1>
    <h2>Gesamtsumme: {{ total }}</h2>

    <h2>Frühere Einträge</h2>
    <ul>
        {% for entry in entries %}
            <li>
                Datum: {{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} - Summe: {{ entry.total }}
            </li>
        {% endfor %}
    </ul>
</body>
</html>

Anpassungen in main.py:

Passe den Endpunkt / und /submit an, um die Liste der Einträge an das Template zu übergeben.

# Im main.py nach dem Import der Einträge
@app.get("/", response_class=HTMLResponse)
def read_form(request: Request, token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
    user = get_current_user(token, db)
    latest_entry = db.query(models.Entry).filter(models.Entry.user_id == user.id).order_by(models.Entry.timestamp.desc()).first()
    data = latest_entry.data if latest_entry else {}
    total = latest_entry.total if latest_entry else 0
    entries = db.query(models.Entry).filter(models.Entry.user_id == user.id).order_by(models.Entry.timestamp.desc()).all()
    logger.info(f"Formularseite aufgerufen von Benutzer: {user.username}")
    return templates.TemplateResponse("form.html", {"request": request, "data": data, "total": total, "entries": entries})

Stelle sicher, dass beim Absenden von /submit die Liste der Einträge aktualisiert und an das Template zurückgegeben wird.


9. Drei FastAPI-Apps auf unterschiedlichen Subdomains (Nginx Reverse Proxy)

9.1 Portzuweisung

Weisen jeder App einen eigenen internen Port zu:

  • App1: 127.0.0.1:8001

  • App2: 127.0.0.1:8002

  • App3: 127.0.0.1:8003

9.2 Erstellen von start.sh für jede App

Für jede App erstellst du ein separates Startskript. Hier wird app1 als Beispiel gezeigt.

# ~/fastapi_projekte/app1/start.sh
#!/bin/bash
source ~/fastapi_projekte/app1/venv/bin/activate
exec gunicorn -k uvicorn.workers.UvicornWorker app.main:app --bind 127.0.0.1:8001 --log-file ~/fastapi_projekte/logs/app1.log

Mache das Skript ausführbar:

chmod +x ~/fastapi_projekte/app1/start.sh

Wiederhole dies für app2 und app3, ändere jedoch die Ports entsprechend:

# app2/start.sh
#!/bin/bash
source ~/fastapi_projekte/app2/venv/bin/activate
exec gunicorn -k uvicorn.workers.UvicornWorker app.main:app --bind 127.0.0.1:8002 --log-file ~/fastapi_projekte/logs/app2.log

# app3/start.sh
#!/bin/bash
source ~/fastapi_projekte/app3/venv/bin/activate
exec gunicorn -k uvicorn.workers.UvicornWorker app.main:app --bind 127.0.0.1:8003 --log-file ~/fastapi_projekte/logs/app3.log

9.3 Erstellen von Systemd-Service-Dateien

Für jede App erstellst du eine separate Systemd-Service-Datei.

  1. App1-Service erstellen: sudo nano /etc/systemd/system/app1.service Inhalt: [Unit] Description=Gunicorn instance for App1 After=network.target [Service] User=dein_benutzername Group=www-data WorkingDirectory=/home/dein_benutzername/fastapi_projekte/app1 ExecStart=/home/dein_benutzername/fastapi_projekte/app1/start.sh Restart=always [Install] WantedBy=multi-user.target Wichtig: Ersetze dein_benutzername mit deinem tatsächlichen Benutzernamen.

  2. App2- und App3-Service-Dateien erstellen: sudo nano /etc/systemd/system/app2.service sudo nano /etc/systemd/system/app3.service Inhalt analog zu app1.service, ändere jedoch den Namen und Pfad entsprechend.

  3. Services starten und aktivieren: sudo systemctl start app1 sudo systemctl enable app1 sudo systemctl start app2 sudo systemctl enable app2 sudo systemctl start app3 sudo systemctl enable app3

  4. Status der Services überprüfen: sudo systemctl status app1 sudo systemctl status app2 sudo systemctl status app3 Stelle sicher, dass alle Services ohne Fehler laufen.

9.4 Nginx als Reverse Proxy konfigurieren

Für jede Subdomain erstellst du eine separate Nginx-Konfigurationsdatei.

  1. App1-Nginx-Konfiguration: sudo nano /etc/nginx/sites-available/app1 Inhalt: server { listen 80; server_name app1.deinedomain.com; access_log /home/dein_benutzername/fastapi_projekte/logs/nginx_app1_access.log; error_log /home/dein_benutzername/fastapi_projekte/logs/nginx_app1_error.log; location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /home/dein_benutzername/fastapi_projekte/app1/app/static/; } }

  2. App2- und App3-Nginx-Konfigurationen erstellen: Erstelle ähnliche Dateien für app2 und app3, passe jedoch server_name, proxy_pass und access_log/error_log entsprechend an. sudo nano /etc/nginx/sites-available/app2 sudo nano /etc/nginx/sites-available/app3 Beispiel für app2: server { listen 80; server_name app2.deinedomain.com; access_log /home/dein_benutzername/fastapi_projekte/logs/nginx_app2_access.log; error_log /home/dein_benutzername/fastapi_projekte/logs/nginx_app2_error.log; location / { proxy_pass http://127.0.0.1:8002; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /home/dein_benutzername/fastapi_projekte/app2/app/static/; } }

  3. Symbolische Links erstellen und Nginx aktivieren: sudo ln -s /etc/nginx/sites-available/app1 /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/app2 /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/app3 /etc/nginx/sites-enabled/

  4. Nginx-Konfiguration testen und neu starten: sudo nginx -t sudo systemctl restart nginx


10. Logging-Konzept für jede Anwendung (Backend- und Nginx-Logging)

10.1 Backend-Logging (FastAPI)

  • Logging-Konfiguration: Bereits in logging_config.py definiert.
  • Integration in main.py: Wird durch den Import und die Initialisierung von logger sichergestellt.

Beispiel in main.py:

logger.info("Root endpoint aufgerufen")
logger.warning("Warnung: Ungültige Eingabe")
logger.error("Fehler beim Verarbeiten der Anfrage")

10.2 Nginx-Logging

Standardmäßig speichert Nginx Zugriffs- und Fehlerlogs in /var/log/nginx/. In den Subdomain-spezifischen Konfigurationen haben wir jedoch separate Log-Dateien definiert.

Beispiel für app1:

access_log /home/dein_benutzername/fastapi_projekte/logs/nginx_app1_access.log;
error_log /home/dein_benutzername/fastapi_projekte/logs/nginx_app1_error.log;

10.3 Log-Rotation konfigurieren

Um sicherzustellen, dass die Log-Dateien nicht unbegrenzt wachsen, nutzen wir logrotate.

  1. Logrotate-Konfiguration erstellen: sudo nano /etc/logrotate.d/nginx_subdomains

  2. Inhalt hinzufügen:/home/dein_benutzername/fastapi_projekte/logs/nginx_*_access.log /home/dein_benutzername/fastapi_projekte/logs/nginx_*_error.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data adm sharedscripts postrotate systemctl reload nginx > /dev/null 2>&1 || true endscript } /home/dein_benutzername/fastapi_projekte/logs/app*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data adm sharedscripts postrotate systemctl restart app1 >/dev/null 2>&1 || true systemctl restart app2 >/dev/null 2>&1 || true systemctl restart app3 >/dev/null 2>&1 || true endscript }Erklärung:

    • Nginx-Logs: Rotieren täglich, bewahren 14 Tage auf, komprimieren alte Logs.

    • App-Logs: Gleiches Prinzip, mit Neustart der Dienste nach der Rotation (optional).

  3. Test der Log-Rotation: sudo logrotate --debug /etc/logrotate.d/nginx_subdomains Stelle sicher, dass keine Fehler auftreten.


11. SSL-Integration mit Let’s Encrypt

Sichere deine Subdomains mit SSL-Zertifikaten von Let’s Encrypt.

  1. Certbot installieren (falls noch nicht installiert): sudo apt install certbot python3-certbot-nginx -y

  2. SSL-Zertifikate für alle Subdomains anfordern: Du kannst alle drei Subdomains in einem einzigen Zertifikat abdecken. sudo certbot --nginx -d app1.deinedomain.com -d app2.deinedomain.com -d app3.deinedomain.com Alternativ: Zertifikate einzeln für jede Subdomain anfordern. sudo certbot --nginx -d app1.deinedomain.com sudo certbot --nginx -d app2.deinedomain.com sudo certbot --nginx -d app3.deinedomain.com

  3. Folge den Anweisungen von Certbot:

    • Wähle aus, ob du HTTP zu HTTPS umleiten möchtest.

    • Bestätige die Registrierung deiner E-Mail-Adresse.

  4. Automatische Erneuerung testen: sudo certbot renew --dry-run Dies stellt sicher, dass die automatische Erneuerung funktioniert.

  5. Überprüfe die SSL-Installation: Öffne deine Subdomains in einem Browser und stelle sicher, dass HTTPS aktiviert ist und das Zertifikat gültig ist. Beispiel: https://app1.deinedomain.com


12. Beispiele, Testdaten und Prüfung der Funktionsweise

12.1 Benutzerregistrierung testen

Verwende einen HTTP-Client wie curl oder httpie, um die Registrierung zu testen.

Mit curl:

curl -X POST -H "Content-Type: application/json" \
     -d '{"username":"testuser","password":"testpass"}' \
     https://app1.deinedomain.com/register

Erwartete Antwort:

{
  "access_token": "jwt_token_string",
  "token_type": "bearer"
}

12.2 Benutzeranmeldung testen

Mit curl:

curl -X POST -d "username=testuser&password=testpass" \
     https://app1.deinedomain.com/token

Erwartete Antwort:

{
  "access_token": "jwt_token_string",
  "token_type": "bearer"
}

12.3 Formular aufrufen und Daten eingeben

  1. Zugriff auf die Formularseite: Öffne https://app1.deinedomain.com/ in deinem Browser. Da die Authentifizierung über JWT erfolgt, musst du sicherstellen, dass du angemeldet bist. In dieser Anleitung wird angenommen, dass du die JWT-Token-Verwaltung über Cookies oder andere Mechanismen implementierst. Für einfache Tests kannst du den Token manuell in den Headers setzen oder eine einfache Frontend-Authentifizierung implementieren.

  2. Formular ausfüllen:

    • Feld 1: 10

    • Feld 2: 20

    • Absenden: Klicke auf “Absenden”

  3. Erwartete Ergebnisse:

    • Gesamtsumme: 30 wird angezeigt.

    • Eintrag in der Datenbank überprüfen: sudo -i -u postgres psql meine_datenbank SELECT * FROM entries; Erwartete Ausgabe: id | user_id | data | timestamp | total ----+---------+-------------------------+----------------------------+------- 1 | 1 | {"feld1": 10, "feld2": 20} | 2024-12-05 12:34:56.789012 | 30 (1 row)

12.4 Einträge anzeigen und bearbeiten

  1. Zugriff auf die Liste der Einträge: Öffne https://app1.deinedomain.com/entries in deinem Browser oder via curl. Mit curl: curl -H "Authorization: Bearer jwt_token_string" https://app1.deinedomain.com/entries Erwartete Antwort: [ { "id": 1, "data": { "feld1": 10, "feld2": 20 }, "timestamp": "2024-12-05T12:34:56.789012", "total": 30 } ]
  2. Eintrag bearbeiten: Verwende das Formular auf der Hauptseite oder sende eine POST-Anfrage an den Endpunkt /entries/{entry_id}. Mit curl: curl -X POST -d "feld1=15&feld2=25" \ -H "Authorization: Bearer jwt_token_string" \ https://app1.deinedomain.com/entries/1 Erwartete Antwort: { "id": 1, "data": { "feld1": 15, "feld2": 25 }, "timestamp": "2024-12-05T12:34:56.789012", "total": 40 } Datenbank überprüfen: SELECT * FROM entries; Erwartete Ausgabe: id | user_id | data | timestamp | total ----+---------+-------------------------+----------------------------+------- 1 | 1 | {"feld1": 15, "feld2": 25} | 2024-12-05 12:34:56.789012 | 40 (1 row)

12.5 Frontend-Authentifizierung (Optional)

Für eine benutzerfreundlichere Erfahrung kannst du eine einfache Frontend-Authentifizierung implementieren, die JWT-Tokens speichert und in den Headers von Anfragen verwendet. Alternativ kannst du Sessions oder Cookies verwenden.


13. Skalierung, Erweiterung und Best Practices

13.1 Umgebungsvariablen sicher verwalten

Verwende .env-Dateien für jede Anwendung, um sensible Informationen sicher zu verwalten.

Beispiel .env für App1:

SECRET_KEY=dein_geheimer_schluessel
DATABASE_URL=postgresql://mein_user:starkes_passwort@localhost/meine_datenbank

13.2 Horizontale Skalierung

  • Mehrere Worker: Erhöhe die Anzahl der Gunicorn-Worker für jede App, um die Last besser zu verteilen. Beispiel in start.sh: exec gunicorn -k uvicorn.workers.UvicornWorker app.main:app --bind 127.0.0.1:8001 --workers 4 --log-file ~/fastapi_projekte/logs/app1.log
  • Mehrere Server: Füge weitere Server hinzu und verwalte den Load Balancing über Nginx oder andere Load Balancer.

13.3 Containerisierung mit Docker (Optional)

Verwende Docker, um deine Anwendungen zu containerisieren, was die Portabilität und Skalierbarkeit erhöht.

  1. Dockerfile erstellen: # ~/fastapi_projekte/app1/Dockerfile FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --upgrade pip RUN pip install -r requirements.txt COPY . . CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app.main:app", "--bind", "0.0.0.0:8001"]

  2. Docker-Image bauen und Container starten: docker build -t app1-image . docker run -d --name app1-container -p 8001:8001 app1-image

  3. Nginx entsprechend anpassen: Ändere proxy_pass in der Nginx-Konfiguration auf den Docker-Container. proxy_pass http://localhost:8001;

13.4 Sicherheitsüberlegungen

  • SECRET_KEY sicher aufbewahren: Halte den SECRET_KEY geheim und nutze sichere Methoden zur Verwaltung von Umgebungsvariablen.

  • Firewall: Stelle sicher, dass nur notwendige Ports (80, 443) offen sind.

  • Regelmäßige Updates: Halte dein System und deine Pakete aktuell.

  • Eingabevalidierung: Stelle sicher, dass alle Benutzereingaben validiert werden, um Sicherheitslücken wie SQL-Injection oder XSS zu vermeiden.

  • HTTPS verwenden: Durch die Integration von Let’s Encrypt wird die Kommunikation verschlüsselt.

13.5 Monitoring und Fehlerbehandlung

  • Monitoring-Tools: Implementiere Tools wie Prometheus und Grafana zur Überwachung der Anwendungen und des Servers.

  • Alerting: Richte Alerts ein, um über kritische Fehler oder Ausfälle informiert zu werden.

  • Logging erweitern: Nutze strukturierte Logs und zentralisiere Logs ggf. mit Tools wie ELK Stack (Elasticsearch, Logstash, Kibana) oder Grafana Loki.

13.6 Backup der Datenbank

  • Regelmäßige Backups: Verwende pg_dump, um regelmäßige Backups deiner PostgreSQL-Datenbank zu erstellen. pg_dump meine_datenbank > ~/fastapi_projekte/backups/meine_datenbank_$(date +%F).sql
  • Automatisierung: Erstelle Cron-Jobs, um Backups automatisch zu erstellen und zu archivieren.

13.7 Erweiterungen und neue Features

  • Neue Felder im Formular: Aktualisiere das Template und die EntryCreate-Schema entsprechend.

  • Zusätzliche Endpunkte: Füge neue Routen in main.py hinzu oder verwende FastAPI-Router für eine bessere Strukturierung.

  • Rollen und Berechtigungen: Implementiere weitere Sicherheitsmechanismen wie Benutzerrollen und Berechtigungen.


14. Zusammenfassung

In dieser umfassenden Anleitung hast du gelernt, wie du:

  1. Einen Ubuntu 22.04 Server für drei getrennte FastAPI-Apps mit PostgreSQL, Nginx und Let’s Encrypt SSL einrichtest.

  2. Benutzerregistrierung, JWT-Authentifizierung, Formulare mit vorbefüllten Werten und Summenberechnung in jeder App integrierst.

  3. Logging sowohl auf Anwendungsebene (mit Python-Logging) als auch auf Webserver-Ebene (Nginx) implementierst.

  4. Deine Anwendungen auf mehrere Subdomains verteilst und sicher per HTTPS erreichst.

  5. Das System wartbar, erweiterbar und skalierbar aufsetzt, indem du Konfigurationen trennst, Umgebungsvariablen nutzt und Best Practices befolgst.

Diese Anleitung bietet ein robustes Grundgerüst, das du nach Bedarf erweitern und anpassen kannst. Mit den bereitgestellten Beispielen und Testdaten kannst du die Funktionsweise überprüfen und sicherstellen, dass alles korrekt konfiguriert ist.


Fertig! Du hast nun eine vollständige, integrierte und erweiterbare Beispielkonfiguration mit allen zuvor genannten Anforderungen erstellt. Falls du weitere Fragen hast oder spezifische Hilfe bei bestimmten Schritten benötigst, stehe ich gerne zur Verfügung!

Weiterlesen .