Hausi — Setup von Null

Komplette Anleitung um Hausi auf einem Proxmox-LXC-Container aufzusetzen. Zeit: ca. 60–90 Minuten.

Voraussetzungen

  • Proxmox VE 8.x oder neuer mit freier Container-ID
  • Debian 13 LXC-Template (oder anderer Debian/Ubuntu-Klon)
  • Interne IP-Adresse für den Container (z.B. 192.168.178.124)
  • SSH-Zugriff als root

1. Container anlegen

In der Proxmox-Web-UI einen neuen Container erstellen:

  • Hostname: hausi
  • Template: Debian 13
  • RAM: 1024 MB minimum, 2048 MB empfohlen
  • Disk: 8–16 GB
  • Netzwerk: feste IP im LAN, eth0/vmbr0
  • Unprivilegiert: ja (empfohlen)
  • Nesting: aktivieren falls Docker später

2. System vorbereiten

Im Container per SSH einloggen und folgende Befehle ausführen:

# Locale setzen (de_DE.UTF-8)
sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen
locale-gen
update-locale LANG=de_DE.UTF-8

# Falls Apache/PHP vorinstalliert sind: weg damit
apt-get remove --purge -y apache2 apache2-* php* 2>/dev/null
apt-get autoremove -y

# Basis-Pakete
apt-get update && apt-get upgrade -y
apt-get install -y curl wget git nano htop ufw \
                    build-essential python3-dev \
                    libjpeg-dev zlib1g-dev libfreetype6-dev \
                    libpango-1.0-0 libpangoft2-1.0-0 \
                    libcairo2 libffi-dev

# Firewall
ufw allow from 192.168.178.0/24 to any port 22 proto tcp
ufw allow from 192.168.178.0/24 to any port 8000 proto tcp
ufw default deny incoming
ufw --force enable

3. MariaDB installieren

apt-get install -y mariadb-server mariadb-client
systemctl enable --now mariadb

# Root-Passwort und Sicherheits-Setup
mariadb-secure-installation

# Datenbank und User anlegen (Passwort hausi_app durch was Sicheres ersetzen!)
mariadb -u root -p << 'EOF'
CREATE DATABASE hausverwaltung CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'hausi_app'@'localhost' IDENTIFIED BY 'DEIN-DB-PASSWORT';
GRANT ALL PRIVILEGES ON hausverwaltung.* TO 'hausi_app'@'localhost';
FLUSH PRIVILEGES;
EOF

4. Python + Backend einrichten

apt-get install -y python3 python3-venv python3-pip

mkdir -p /opt/hausi/{backend,frontend/static,sql,backups,backend/app/routers,backend/app/templates}
cd /opt/hausi/backend

python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip

# Alle Python-Pakete
pip install "fastapi[standard]" sqlalchemy pymysql weasyprint jinja2 \
            pydantic-settings email-validator Pillow pypdf \
            bcrypt pyotp itsdangerous "qrcode[pil]"

5. .env-Datei anlegen

# Secret-Key für Sessions generieren
KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")

cat > /opt/hausi/backend/.env << EOF
# Datenbank
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=hausverwaltung
DB_USER=hausi_app
DB_PASSWORD=DEIN-DB-PASSWORT

# App
APP_HOST=0.0.0.0
APP_PORT=8000
APP_DEBUG=false
APP_SECRET_KEY=$KEY
COOKIE_SECURE=false

# Briefkopf für PDFs
PDF_VERMIETER_NAME=Dein Name
PDF_VERMIETER_STRASSE=Beispielstrasse 1
PDF_VERMIETER_PLZ_ORT=65000 Musterstadt
EOF

chmod 600 /opt/hausi/backend/.env

6. Datenbankschema anlegen

14 Tabellen werden benötigt. Das vollständige Schema liegt in /opt/hausi/sql/001_schema.sql. Kerntabellen:

TabelleZweck
haeuserMietshäuser mit Adresse, Gesamtfläche, Anzahl Einheiten
einheitenWohnungen mit Wohnfläche, Lage, Zimmer
mietverhaeltnisseMieter pro Wohnung mit Mietbeginn/-ende, Kaltmiete, NK-VZ
personen_im_haushaltHistorisierte Personenzahl mit Gültigkeitsdaten
kostenarten18 Standard-Kostenarten mit Umlageschlüssel
abrechnungsperiodenPro Haus und Jahr ein Eintrag
kostenpostenEinzelne Rechnungen im Jahr
techem_belegeHeiz-/Warmwasserkosten pro Mieter/Periode
vorauszahlungenSumme NK-VZ pro Mieter/Periode
belegeBeleg-Uploads mit Foto/PDF als LONGBLOB
benutzerLogin-User mit bcrypt-Hash und TOTP-Secret
system_einstellungenKey-Value-Store für Briefkopf und Logo

7. Applikations-Code deployen

Den Code aus dem Git-Repo bzw. Backup nach /opt/hausi/ kopieren. Struktur:

/opt/hausi/
├── backend/
│   ├── .env                    # geheim, chmod 600
│   ├── venv/                   # Python virtualenv
│   └── app/
│       ├── main.py             # FastAPI-App + Routing
│       ├── config.py           # Settings aus .env
│       ├── database.py         # SQLAlchemy-Engine
│       ├── models.py           # 14 ORM-Klassen
│       ├── auth.py             # bcrypt, TOTP, Sessions
│       ├── bild_helper.py      # Pillow + PDF-Konvertierung
│       ├── init_user.py        # Erst-Admin-Skript
│       ├── routers/
│       │   ├── haeuser.py einheiten.py mietverhaeltnisse.py
│       │   ├── kostenarten.py personen.py perioden.py
│       │   ├── kostenposten.py zahlungen.py berechnung.py
│       │   ├── pdf.py einstellungen.py belege.py
│       │   └── auth.py benutzer.py
│       └── templates/
│           └── abrechnung_pdf.html
└── frontend/
    ├── index.html haeuser.html einheiten.html mieter.html
    ├── abrechnung.html periode_detail.html berechnung.html
    ├── einstellungen.html belege.html benutzer.html
    ├── login.html setup_2fa.html anleitung.html
    └── static/
        ├── hausi-logo.svg
        └── hilfe-button.js

8. systemd-Service einrichten

cat > /etc/systemd/system/hausi.service << 'EOF'
[Unit]
Description=Hausi - Hausverwaltung FastAPI Backend
Documentation=http://192.168.178.124:8000/docs
After=network.target mariadb.service
Requires=mariadb.service

[Service]
Type=exec
User=root
WorkingDirectory=/opt/hausi/backend
EnvironmentFile=/opt/hausi/backend/.env
ExecStart=/opt/hausi/backend/venv/bin/uvicorn app.main:app \
          --host 0.0.0.0 --port 8000 --workers 2 --proxy-headers
Restart=on-failure
RestartSec=5

# Sicherheit
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now hausi
systemctl status hausi --no-pager

9. Ersten Admin-User anlegen

cd /opt/hausi/backend
source venv/bin/activate
python -m app.init_user

# Interaktive Eingabe:
# - Benutzername: steffen
# - Vollname: Steffen Catta
# - E-Mail: scatta@gmx.de
# - Passwort: mindestens 10 Zeichen

10. Erster Login + 2FA

  1. Browser öffnen: http://192.168.178.124:8000/login
  2. Benutzername + Passwort eingeben → „Weiter"
  3. QR-Code mit Bitwarden, Google Authenticator oder anderer TOTP-App scannen
  4. 6-stelligen Code aus der App eintragen → „Aktivieren"
  5. Du landest auf dem Dashboard

Fertig!

Hausi läuft jetzt unter http://192.168.178.124:8000/. Optional: nginx mit HTTPS davorschalten (siehe Architektur).

Architektur

Wie Hausi technisch aufgebaut ist und wie die Komponenten zusammenhängen.

Stack-Übersicht

SchichtTechnologieZweck
HostingProxmox LXC, Debian 13Container-Isolation
DatenbankMariaDB 11.8Relationale Persistenz, utf8mb4
BackendPython 3.13 + FastAPIREST-API + HTML-Routes
ORMSQLAlchemy 2.0 (Mapped[])Mapping zwischen DB und Python
PDF-EngineWeasyPrint + Jinja2HTML→PDF mit Style
PDF-MergepypdfBelege ans Mieter-PDF anhängen
BilderPillowJPG-Optimierung, Thumbnails
Authbcrypt + pyotp + itsdangerousLogin, 2FA, Session-Cookies
FrontendTailwind CDN + Alpine.jsSingle-Page-Feeling ohne Build
IconsTabler Icons Webfont5800+ Icons als CSS-Klassen

Datenmodell

Die 14 Tabellen und ihre wichtigsten Beziehungen:

haeuser (1)──< einheiten (N)──< mietverhaeltnisse (N)
                                            │
                                            └──< personen_im_haushalt (historisiert)

haeuser (1)──< abrechnungsperioden (N)──< kostenposten (N)
                                                    │
                                                    └──< belege (N)

mietverhaeltnisse ──< vorauszahlungen, techem_belege (pro Periode)

kostenarten (18 Standards) ────── kostenposten (FK)

system_einstellungen (Key-Value, LONGTEXT)
benutzer (Auth)

Berechnungs-Logik

Wie wird der Anteil eines Mieters berechnet?

1. Mietzeit-Faktor: Wenn ein Mieter nicht das ganze Jahr in der Wohnung war, wird sein Anteil zeitanteilig berechnet.

mietzeit_faktor = tage_mieter_in_periode / tage_periode

2. Umlageschlüssel: Jede Kostenart hat einen Schlüssel:

  • flaeche — anteilig nach Wohnfläche
  • personen — Personentage-Verteilung (Personenzahl × Tage)
  • einheiten — gleichmäßig auf alle Einheiten
  • verbrauch — nach Verbrauchsdaten (Wasser, Strom)
  • fix_pro_mieter — pauschal pro Mieter (selten verwendet)

3. Beispiel Grundsteuer (flaeche):

anteil = (wohnflaeche_mieter / gesamtflaeche_haus) * kostenposten_betrag * mietzeit_faktor

4. Saldo-Berechnung:

gesamt_kosten = summe_kalt_nk + techem_heizkosten + techem_warmwasser
saldo = gesamt_kosten - vorauszahlungen

Positiver Saldo = Nachzahlung, negativer = Guthaben.

Request-Flow

Browser
   │
   ▼
GET / (Hausi-Frontend)
   │
   ▼ FastAPI prüft Session-Cookie "hausi_session"
   │
   ├─ Cookie gültig?  →  FileResponse(index.html)
   │
   └─ Cookie fehlt/abgelaufen?  →  RedirectResponse(/login, 303)
        │
        ▼
   Browser zeigt /login.html
   User gibt Username + Passwort ein
   POST /api/auth/login
        │
        ▼ benutzer_authentifizieren()
        │
        ├─ Passwort falsch?  →  fehlversuche++ → ggf. Sperre 15min
        ├─ 2FA noch nicht aktiv?  →  benoetigt_2fa_setup=true → /setup-2fa
        ├─ 2FA-Code fehlt?  →  benoetigt_2fa=true → Frontend zeigt Code-Eingabe
        ├─ 2FA-Code falsch?  →  Fehler zurück
        └─ Alles OK  →  Cookie setzen, RedirectResponse(/)

nginx-Reverse-Proxy (optional)

Für öffentliche Erreichbarkeit per HTTPS — z.B. auf einem separaten LXC. Beispiel-Config:

server {
    listen 443 ssl http2;
    server_name hausi.example.com;

    ssl_certificate     /etc/letsencrypt/live/hausi.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hausi.example.com/privkey.pem;

    # Rate-Limit auf Login
    location = /api/auth/login {
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://192.168.178.124:8000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }

    location / {
        proxy_pass http://192.168.178.124:8000;
        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 https;
        client_max_body_size 50M;
    }
}
Wichtig: Sobald HTTPS aktiv ist, in .env COOKIE_SECURE=true setzen und Hausi neu starten.

API-Referenz

Alle REST-Endpoints. Vollständige interaktive Doku unter /docs (Swagger UI).

Auth: Außer den /api/auth/*-Endpoints erfordern alle Aufrufe den Cookie hausi_session.

Auth

MethodePfadBeschreibung
POST/api/auth/loginLogin mit username + passwort + optional totp_code
POST/api/auth/logoutSession beenden
GET/api/auth/meAktuell eingeloggten User abfragen
POST/api/auth/setup-2fa/initQR-Code für 2FA-Einrichtung anfordern
POST/api/auth/setup-2fa/confirm2FA mit erstem Code aktivieren
POST/api/auth/passwort-aendernEigenes Passwort ändern

Häuser, Einheiten, Mieter

MethodePfadBeschreibung
GET POST/api/haeuserListe / Anlegen
GET PUT DEL/api/haeuser/{id}Detail / Ändern / Löschen
GET POST/api/einheitenWohnungen
GET POST/api/mietverhaeltnisseMieter (mit aktive-Filter)
GET POST/api/personenPersonenzahl-Historie

Abrechnung

MethodePfadBeschreibung
GET POST/api/kostenarten18 Standard-Kostenarten
GET POST/api/periodenAbrechnungsperioden
GET POST/api/kostenposten?periode_id=NPosten pro Periode
GET POST/api/vorauszahlungenNK-VZ pro Mieter/Periode
GET POST/api/techem-belegeHeiz-/Warmwasser-Werte
GET/api/berechnung/{periode_id}Volle Berechnung als JSON
GET/api/abrechnung/{periode_id}/pdfSammel-PDF aller Mieter
GET/api/abrechnung/{periode_id}/pdf?mit_belegen=truePDF mit Belegen gemergt
GET/api/abrechnung/{periode_id}/pdf/{mv_id}Einzel-PDF für einen Mieter

Belege + Einstellungen

MethodePfadBeschreibung
GET/api/belegeListe, Filter: nur_unzugeordnet, kostenposten_id
POST/api/belege/uploadMultipart-Upload (PDF/JPG/PNG/HEIC, max 25 MB)
GET/api/belege/{id}/dateiOriginal-Datei
GET/api/belege/{id}/thumbnail200px-Thumbnail als JPG
PUT/api/belege/{id}Bezeichnung, Datum, kostenposten_id, Bemerkung
GET POST/api/einstellungenBriefkopf + Bankdaten
POST/api/einstellungen/logo/uploadLogo (PNG/JPG, max 5 MB)
GET/api/einstellungen/logo/statusOb Logo vorhanden + Größe

Benutzerverwaltung (admin only)

MethodePfadBeschreibung
GET POST/api/benutzerListe / Anlegen
PUT/api/benutzer/{id}vollname, email, rolle, ist_aktiv
POST/api/benutzer/{id}/passwort-resetPasswort durch Admin setzen
POST/api/benutzer/{id}/2fa-reset2FA zurücksetzen — User richtet beim Login neu ein
DELETE/api/benutzer/{id}User löschen

Troubleshooting

Häufige Probleme + Lösungen.

Hausi startet nicht — systemd zeigt failed

Logs ansehen:

journalctl -u hausi -n 50 --no-pager

Häufige Ursachen:

  • ModuleNotFoundError → Pip-Pakete fehlen: pip install ... im venv
  • AttributeError: module has no attribute 'router' → Import-Pfade in main.py prüfen
  • OperationalError: Access denied → DB-Passwort in .env falsch
  • APP_SECRET_KEY ist nicht konfiguriert → 64-Zeichen-Hex-Key in .env mit APP_SECRET_KEY=-Präfix
"Data too long for column 'wert'" beim Logo-Upload

Die Spalte wert ist als TEXT definiert (max 65 KB), das Logo als base64 ist aber meist größer:

mariadb hausverwaltung -e \
"ALTER TABLE system_einstellungen MODIFY COLUMN wert LONGTEXT NULL;"
Login klappt nicht — "Konto gesperrt" trotz korrektem Passwort

Brute-Force-Schutz: nach 5 Fehlversuchen 15 Min Sperre. Manuell freischalten:

mariadb hausverwaltung -e \
"UPDATE benutzer SET fehlversuche=0, gesperrt_bis=NULL WHERE username='steffen';"
2FA-App verloren — wie wieder reinkommen?

Direkt in der DB 2FA zurücksetzen — beim nächsten Login wird neu eingerichtet:

mariadb hausverwaltung -e \
"UPDATE benutzer SET totp_aktiviert=FALSE, totp_secret=NULL WHERE username='steffen';"
PDF-Erzeugung schlägt mit Header-Encoding-Fehler fehl

HTTP-Header sind nur latin-1 — Umlaute und Gedankenstriche im Dateinamen crashen. Die Funktion _ascii_dateiname() in routers/pdf.py sollte das abfangen. Wenn nicht: prüfen, ob die Periode/Mieter-Bezeichnung Sonderzeichen enthält.

Belege-Upload schlägt mit "Datei zu groß" fehl

Max 25 MB. Bei iPhone-Bildern hilft die Pillow-Optimierung (auto auf 1600px JPG). Bei PDFs: vorher z.B. pdftk oder Smallpdf komprimieren.

Falls nginx davor: client_max_body_size 50M; im Server-Block setzen.

Webfont/Icons werden nicht geladen

Tailwind + Alpine + Tabler kommen vom CDN — der Container braucht Internet-Zugang. Bei Air-Gap-Setup: Assets lokal hosten und in frontend/static/ ablegen, dann die <script>-Tags umstellen.

Browser zeigt "Unsicher" / "Verbindung nicht privat"

Hausi läuft per default auf HTTP. Im LAN ist das ok, öffentlich brauchst du HTTPS:

  • nginx-Reverse-Proxy davorschalten (siehe Architektur)
  • Let's-Encrypt-Zertifikat per certbot
  • In .env COOKIE_SECURE=true setzen

Backup & Restore

Wie du Hausi sicherst und im Notfall wiederherstellst.

Was muss gesichert werden?

PfadInhaltBackup-Pflicht
MariaDB-DBAlle Mieter, Kosten, Belege, UserJA — täglich
/opt/hausi/backend/.envDB-Passwort, Secret-KeyJA — wöchentlich
/opt/hausi/backend/app/Python-CodeBei Änderungen (Git!)
/opt/hausi/frontend/HTML/JSBei Änderungen (Git!)
venv/Python-PaketeNicht nötig — neu installierbar
Belege sind in der DB als LONGBLOB. Ein mariadb-dump sichert sie also mit. Bei vielen Belegen kann der Dump groß werden (1 MB pro Beleg-Foto ist normal).

Manuelles Backup

DATUM=$(date +%Y-%m-%d_%H-%M)
mkdir -p /opt/hausi/backups
mariadb-dump --single-transaction --routines --triggers hausverwaltung \
  > /opt/hausi/backups/hausverwaltung_${DATUM}.sql

# .env mitsichern
cp /opt/hausi/backend/.env /opt/hausi/backups/env_${DATUM}.txt

ls -lh /opt/hausi/backups/

Automatisches Backup per Cron

# Backup-Skript anlegen
cat > /usr/local/bin/hausi-backup.sh << 'EOF'
#!/bin/bash
DATUM=$(date +%Y-%m-%d)
BACKUP_DIR=/opt/hausi/backups
mkdir -p $BACKUP_DIR

# DB-Dump
mariadb-dump --single-transaction --routines --triggers hausverwaltung \
  | gzip > $BACKUP_DIR/hausverwaltung_$DATUM.sql.gz

# .env (alle 7 Tage)
if [ $(date +%u) -eq 7 ]; then
  cp /opt/hausi/backend/.env $BACKUP_DIR/env_$DATUM.txt
fi

# Alte Backups löschen (älter als 30 Tage)
find $BACKUP_DIR -name "hausverwaltung_*.sql.gz" -mtime +30 -delete
find $BACKUP_DIR -name "env_*.txt" -mtime +90 -delete

echo "$(date): Backup OK" >> /var/log/hausi-backup.log
EOF

chmod +x /usr/local/bin/hausi-backup.sh

# Cron-Job: täglich um 03:00
echo "0 3 * * * root /usr/local/bin/hausi-backup.sh" \
  > /etc/cron.d/hausi-backup

Off-Site-Backup (empfohlen!)

Backups vom Container weg auf NAS/Cloud sichern, sonst sind sie bei Container-Verlust auch weg.

# Beispiel: tägliche rsync auf NAS-Share
apt-get install -y rsync

cat >> /usr/local/bin/hausi-backup.sh << 'EOF'

# NAS-Mount, dann rsync
mount -t cifs //nas.local/backups /mnt/nas \
  -o username=backup,password=XXX,vers=3.0
rsync -av $BACKUP_DIR/ /mnt/nas/hausi/
umount /mnt/nas
EOF

Restore — Daten wiederherstellen

Achtung: Vor dem Restore unbedingt den Hausi-Service stoppen, damit nicht parallel geschrieben wird.
systemctl stop hausi

# DB komplett zurücksetzen
mariadb -e "DROP DATABASE hausverwaltung;"
mariadb -e "CREATE DATABASE hausverwaltung CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb -e "GRANT ALL ON hausverwaltung.* TO 'hausi_app'@'localhost';"

# Backup einspielen (entweder .sql oder .sql.gz)
mariadb hausverwaltung < /opt/hausi/backups/hausverwaltung_2026-05-26_10-21.sql
# bzw. bei gz:
gunzip -c /opt/hausi/backups/hausverwaltung_2026-05-26.sql.gz \
  | mariadb hausverwaltung

# Service wieder starten
systemctl start hausi
systemctl status hausi --no-pager

Disaster Recovery — Komplett neu aufsetzen

Wenn der ganze Container verloren ist:

  1. Neuen Container per Setup-Anleitung aufsetzen (Tab Setup → Schritte 1-5)
  2. Datenbank und User anlegen (Schritt 3) — gleicher User wie zuvor!
  3. Python-venv + Pakete (Schritt 4)
  4. .env aus Backup wiederherstellen (gleicher APP_SECRET_KEY!)
  5. Code aus Git holen — /opt/hausi/backend/app und /opt/hausi/frontend
  6. DB-Backup einspielen wie oben
  7. systemd-Service einrichten (Schritt 8)
  8. Service starten — fertig

Tipp: Restore regelmäßig testen!

Ein Backup, das du nie wiederhergestellt hast, ist kein Backup. Einmal im Quartal in einem Test-Container einspielen und prüfen, ob Login + Berechnung funktioniert.

🔒 © by GETsys IT · Hausi-Doku · Mai 2026