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:
| Tabelle | Zweck |
|---|---|
haeuser | Mietshäuser mit Adresse, Gesamtfläche, Anzahl Einheiten |
einheiten | Wohnungen mit Wohnfläche, Lage, Zimmer |
mietverhaeltnisse | Mieter pro Wohnung mit Mietbeginn/-ende, Kaltmiete, NK-VZ |
personen_im_haushalt | Historisierte Personenzahl mit Gültigkeitsdaten |
kostenarten | 18 Standard-Kostenarten mit Umlageschlüssel |
abrechnungsperioden | Pro Haus und Jahr ein Eintrag |
kostenposten | Einzelne Rechnungen im Jahr |
techem_belege | Heiz-/Warmwasserkosten pro Mieter/Periode |
vorauszahlungen | Summe NK-VZ pro Mieter/Periode |
belege | Beleg-Uploads mit Foto/PDF als LONGBLOB |
benutzer | Login-User mit bcrypt-Hash und TOTP-Secret |
system_einstellungen | Key-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
- Browser öffnen:
http://192.168.178.124:8000/login - Benutzername + Passwort eingeben → „Weiter"
- QR-Code mit Bitwarden, Google Authenticator oder anderer TOTP-App scannen
- 6-stelligen Code aus der App eintragen → „Aktivieren"
- 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
| Schicht | Technologie | Zweck |
|---|---|---|
| Hosting | Proxmox LXC, Debian 13 | Container-Isolation |
| Datenbank | MariaDB 11.8 | Relationale Persistenz, utf8mb4 |
| Backend | Python 3.13 + FastAPI | REST-API + HTML-Routes |
| ORM | SQLAlchemy 2.0 (Mapped[]) | Mapping zwischen DB und Python |
| PDF-Engine | WeasyPrint + Jinja2 | HTML→PDF mit Style |
| PDF-Merge | pypdf | Belege ans Mieter-PDF anhängen |
| Bilder | Pillow | JPG-Optimierung, Thumbnails |
| Auth | bcrypt + pyotp + itsdangerous | Login, 2FA, Session-Cookies |
| Frontend | Tailwind CDN + Alpine.js | Single-Page-Feeling ohne Build |
| Icons | Tabler Icons Webfont | 5800+ 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ächepersonen— Personentage-Verteilung (Personenzahl × Tage)einheiten— gleichmäßig auf alle Einheitenverbrauch— 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;
}
}
.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
| Methode | Pfad | Beschreibung |
|---|---|---|
POST | /api/auth/login | Login mit username + passwort + optional totp_code |
POST | /api/auth/logout | Session beenden |
GET | /api/auth/me | Aktuell eingeloggten User abfragen |
POST | /api/auth/setup-2fa/init | QR-Code für 2FA-Einrichtung anfordern |
POST | /api/auth/setup-2fa/confirm | 2FA mit erstem Code aktivieren |
POST | /api/auth/passwort-aendern | Eigenes Passwort ändern |
Häuser, Einheiten, Mieter
| Methode | Pfad | Beschreibung |
|---|---|---|
GET POST | /api/haeuser | Liste / Anlegen |
GET PUT DEL | /api/haeuser/{id} | Detail / Ändern / Löschen |
GET POST | /api/einheiten | Wohnungen |
GET POST | /api/mietverhaeltnisse | Mieter (mit aktive-Filter) |
GET POST | /api/personen | Personenzahl-Historie |
Abrechnung
| Methode | Pfad | Beschreibung |
|---|---|---|
GET POST | /api/kostenarten | 18 Standard-Kostenarten |
GET POST | /api/perioden | Abrechnungsperioden |
GET POST | /api/kostenposten?periode_id=N | Posten pro Periode |
GET POST | /api/vorauszahlungen | NK-VZ pro Mieter/Periode |
GET POST | /api/techem-belege | Heiz-/Warmwasser-Werte |
GET | /api/berechnung/{periode_id} | Volle Berechnung als JSON |
GET | /api/abrechnung/{periode_id}/pdf | Sammel-PDF aller Mieter |
GET | /api/abrechnung/{periode_id}/pdf?mit_belegen=true | PDF mit Belegen gemergt |
GET | /api/abrechnung/{periode_id}/pdf/{mv_id} | Einzel-PDF für einen Mieter |
Belege + Einstellungen
| Methode | Pfad | Beschreibung |
|---|---|---|
GET | /api/belege | Liste, Filter: nur_unzugeordnet, kostenposten_id |
POST | /api/belege/upload | Multipart-Upload (PDF/JPG/PNG/HEIC, max 25 MB) |
GET | /api/belege/{id}/datei | Original-Datei |
GET | /api/belege/{id}/thumbnail | 200px-Thumbnail als JPG |
PUT | /api/belege/{id} | Bezeichnung, Datum, kostenposten_id, Bemerkung |
GET POST | /api/einstellungen | Briefkopf + Bankdaten |
POST | /api/einstellungen/logo/upload | Logo (PNG/JPG, max 5 MB) |
GET | /api/einstellungen/logo/status | Ob Logo vorhanden + Größe |
Benutzerverwaltung (admin only)
| Methode | Pfad | Beschreibung |
|---|---|---|
GET POST | /api/benutzer | Liste / Anlegen |
PUT | /api/benutzer/{id} | vollname, email, rolle, ist_aktiv |
POST | /api/benutzer/{id}/passwort-reset | Passwort durch Admin setzen |
POST | /api/benutzer/{id}/2fa-reset | 2FA 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 venvAttributeError: module has no attribute 'router'→ Import-Pfade in main.py prüfenOperationalError: Access denied→ DB-Passwort in.envfalschAPP_SECRET_KEY ist nicht konfiguriert→ 64-Zeichen-Hex-Key in.envmitAPP_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
.envCOOKIE_SECURE=truesetzen
Backup & Restore
Wie du Hausi sicherst und im Notfall wiederherstellst.
Was muss gesichert werden?
| Pfad | Inhalt | Backup-Pflicht |
|---|---|---|
MariaDB-DB | Alle Mieter, Kosten, Belege, User | JA — täglich |
/opt/hausi/backend/.env | DB-Passwort, Secret-Key | JA — wöchentlich |
/opt/hausi/backend/app/ | Python-Code | Bei Änderungen (Git!) |
/opt/hausi/frontend/ | HTML/JS | Bei Änderungen (Git!) |
venv/ | Python-Pakete | Nicht nötig — neu installierbar |
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
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:
- Neuen Container per Setup-Anleitung aufsetzen (Tab Setup → Schritte 1-5)
- Datenbank und User anlegen (Schritt 3) — gleicher User wie zuvor!
- Python-venv + Pakete (Schritt 4)
.envaus Backup wiederherstellen (gleicher APP_SECRET_KEY!)- Code aus Git holen —
/opt/hausi/backend/appund/opt/hausi/frontend - DB-Backup einspielen wie oben
- systemd-Service einrichten (Schritt 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