Betrieb und Installation¶
Diese Seite beschreibt, wie du Haben auf einem eigenen Server betreibst: mit Docker Compose (am einfachsten) oder mit Podman Quadlets unter systemd, dazu Umgebungsvariablen, Backup und Updates. Was du danach in der Oberfläche einrichtest, steht in Erste Schritte.
Überblick¶
Haben läuft als drei Container in einem eigenen Netz:
| Container (Compose / Quadlet) | Image | Aufgabe |
|---|---|---|
db / haben-db |
postgres:16 |
Datenbank, Volume db bzw. haben-db |
app / haben-app |
tristanteu/haben:latest |
Die App auf Port 3000, Belegdateien im Volume belege bzw. haben-belege, ERiC im Volume eric bzw. haben-eric, ERiC-Logs im Volume eric-log bzw. haben-eric-log |
caddy / haben-caddy |
caddy:2 |
TLS und Reverse Proxy auf Port 80/443, Zertifikate im Volume caddy bzw. haben-caddy. Läuft bei dir schon ein Reverse Proxy, entfällt er, siehe Eigener Reverse Proxy |
Es gibt zwei Wege, sie zu betreiben:
| Weg | Gut für | Dateien |
|---|---|---|
| Docker Compose | Schnell startklar, Docker oder Podman mit Compose | deploy/compose.yml, deploy/compose.env.example |
| Podman Quadlets | Rootless unter systemd, Secrets statt Datei, täglicher Backup-Timer, podman auto-update |
deploy/quadlet/*, deploy/Caddyfile, deploy/haben.env.example, deploy/backup.sh |
Beide holen die Dateien per curl; das Repository musst du nicht klonen.
Voraussetzungen¶
- Ein Linux-Server (x86_64, wenn du ERiC nutzen willst) mit Docker und Compose 2.23 oder neuer, oder mit Podman und systemd. Die Quadlets sind für den Betrieb als normaler Nutzer (rootless) geschrieben, die Pfade nutzen
%hbzw.~/.config. - Eine Domain, die auf den Server zeigt, und erreichbare Ports 80 und 443. Caddy holt das TLS-Zertifikat selbst. Hast du schon einen Reverse Proxy (Caddy, nginx, Nginx Proxy Manager, Traefik), nutzt du den, siehe Eigener Reverse Proxy.
- HTTPS ist Pflicht: Passkeys und die Installation als App (PWA) funktionieren nur über eine sichere Verbindung.
- Für ELSTER ERiC für Linux x86_64; Haben lädt es auf Wunsch selbst herunter (siehe ERiC einbinden).
- Für das Backup mit Quadlets
resticauf dem Host.
[!NOTE] Nur Podman: Rootless Podman darf Ports unter 1024 standardmäßig nicht öffnen. Entweder du senkst die Grenze (
sysctl net.ipv4.ip_unprivileged_port_start=80) oder du veröffentlichst Caddy auf anderen Ports und leitest weiter. Damit die Dienste ohne angemeldete Sitzung laufen, braucht der Nutzer außerdemloginctl enable-linger.
Image¶
Das fertige Image gibt es für amd64 und arm64 auf Docker Hub und in der GitHub Container Registry:
| Image | Inhalt |
|---|---|
docker.io/tristanteu/haben:latest |
Letzte veröffentlichte Version (Standard in Compose und Quadlet) |
docker.io/tristanteu/haben:0.1 / :0.1.0 |
Feste Version, wenn du Updates selbst steuern willst |
docker.io/tristanteu/haben:edge |
Stand von main, nur zum Ausprobieren |
ghcr.io/firsttris/haben:… |
Dieselben Tags bei GitHub |
ERiC ist nicht enthalten, weil es nicht weitergegeben werden darf. Haben lädt es auf deinen Wunsch direkt von der Finanzverwaltung in das Volume eric (siehe ERiC einbinden), oder du mountest es selbst. ERiC gibt es nur für Linux x86_64: Auf arm64 (z. B. Raspberry Pi) läuft alles außer der ELSTER-Übermittlung.
Selbst bauen geht auch, zum Beispiel für einen eigenen Stand. Dafür brauchst du das Repository; danach in compose.yml image: haben bzw. in haben-app.container Image=localhost/haben:latest eintragen:
git clone https://github.com/firsttris/haben.git && cd haben
podman build -t haben -f Containerfile . # oder docker build
Das Containerfile baut die App in zwei Stufen auf Basis von node:22-bookworm-slim.
Das Image läuft als Nutzer node und setzt diese Werte schon selbst: NODE_ENV=production, PORT=3000, DOCUMENTS_DIR=/var/lib/haben/belege, ERIC_LOG_DIR=/var/lib/haben/eric-log, ERIC_WORKER_PATH und HABEN_EINVOICE_DIR.
Docker Compose¶
mkdir haben && cd haben
curl -O https://raw.githubusercontent.com/firsttris/haben/main/deploy/compose.yml
curl -o .env https://raw.githubusercontent.com/firsttris/haben/main/deploy/compose.env.example
In .env trägst du ein:
| Variable | Inhalt |
|---|---|
DOMAIN |
Domain ohne https://, z. B. haben.example.de. Caddy holt dafür das Zertifikat, die App nutzt https://DOMAIN als BETTER_AUTH_URL |
DB_PASSWORD |
Datenbank-Passwort, nur Buchstaben und Ziffern, weil es in der Verbindungs-URL steht: openssl rand -hex 24 |
AUTH_SECRET |
openssl rand -base64 32 |
ENCRYPTION_KEY |
openssl rand -base64 32, zusätzlich an einem zweiten Ort sichern |
ERIC_HOME, ELSTER_HERSTELLER_ID, ANTHROPIC_API_KEY |
optional, siehe Umgebungsvariablen |
Die Caddy-Konfiguration steckt in compose.yml selbst (mit Sicherheits-Headern und Upload-Grenze wie im Caddyfile). Caddy nimmt Anfragen bis 250 MB an; das reicht für eine Archivdatei bis 100 MB oder ein gutes Dutzend Belege auf einmal. Größere Mengen lädst du in mehreren Schritten hoch oder erhöhst max_size. ERiC lädst du unter Einstellungen › ELSTER › ERiC herunter oder mit ERIC_AUTO_INSTALL=ja in .env beim Start, siehe ERiC einbinden.
Updates: docker compose pull && docker compose up -d. Migrationen laufen beim Start der App.
Backup: Sichere die Datenbank und das Volume mit den Belegen, zum Beispiel per Cronjob:
docker compose exec -T db pg_dump -U haben haben | gzip > haben-$(date +%F).sql.gz
# Ein abgebrochener Dump fällt sonst nicht auf: die letzte Zeile muss „dump complete“ melden
gunzip -c haben-$(date +%F).sql.gz | tail -n 20 | grep -q "PostgreSQL database dump complete" || echo "Dump unvollständig" >&2
docker run --rm -v haben_belege:/belege:ro -v "$PWD":/backup alpine tar czf /backup/belege-$(date +%F).tar.gz -C /belege .
Beides gehört anschließend auf ein zweites System, etwa mit restic wie im Quadlet-Backup unten. Zum Wiederherstellen erst nur docker compose up -d db starten, den Dump mit gunzip -c … | docker compose exec -T db psql -U haben -d haben einspielen, die Belege mit tar xzf ins Volume haben_belege entpacken und danach docker compose up -d ausführen.
Podman Quadlets¶
Die Quadlets betreiben Haben rootless unter systemd: Zugangsdaten als Podman Secrets, ein täglicher Backup-Timer mit restic und Updates über podman auto-update.
Dateien ablegen¶
RAW=https://raw.githubusercontent.com/firsttris/haben/main/deploy
mkdir -p ~/.config/containers/systemd ~/.config/systemd/user ~/.config/haben
for f in haben.network haben-db.volume haben-belege.volume haben-eric-log.volume haben-eric.volume haben-caddy.volume \
haben-db.container haben-app.container haben-caddy.container; do
curl -o ~/.config/containers/systemd/$f "$RAW/quadlet/$f"
done
curl -o ~/.config/systemd/user/haben-backup.service "$RAW/quadlet/haben-backup.service"
curl -o ~/.config/systemd/user/haben-backup.timer "$RAW/quadlet/haben-backup.timer"
curl -o ~/.config/haben/Caddyfile "$RAW/Caddyfile"
curl -o ~/.config/haben/haben.env "$RAW/haben.env.example"
curl -o ~/.config/haben/backup.sh "$RAW/backup.sh" && chmod +x ~/.config/haben/backup.sh
curl -o ~/.config/haben/restore.sh "$RAW/restore.sh" && chmod +x ~/.config/haben/restore.sh
Die .container-, .volume- und .network-Dateien verarbeitet der Quadlet-Generator von Podman. haben-backup.service und haben-backup.timer sind gewöhnliche systemd-Units und gehören deshalb nach ~/.config/systemd/user/.
Secrets anlegen¶
Zugangsdaten liegen nicht in haben.env, sondern als Podman Secrets. Die Quadlets reichen sie als Umgebungsvariablen in die Container.
DBPW="$(openssl rand -hex 24)"
printf '%s' "$DBPW" | podman secret create haben-db-password -
printf 'postgres://haben:%s@haben-db:5432/haben' "$DBPW" | podman secret create haben-database-url -
openssl rand -base64 32 | tr -d '\n' | podman secret create haben-auth-secret -
openssl rand -base64 32 | tr -d '\n' | podman secret create haben-encryption-key -
| Secret | Variable im Container | Inhalt |
|---|---|---|
haben-db-password |
POSTGRES_PASSWORD (in haben-db) |
Passwort des Datenbanknutzers haben |
haben-database-url |
DATABASE_URL |
Verbindung zur Datenbank, Passwort wie oben |
haben-auth-secret |
BETTER_AUTH_SECRET |
Signiert die Sitzungen |
haben-encryption-key |
HABEN_ENCRYPTION_KEY |
Schlüssel für ELSTER-Zertifikat und Lexoffice-API-Schlüssel |
haben-anthropic-key (optional) |
ANTHROPIC_API_KEY |
Für die KI-Auslesung, siehe unten |
[!IMPORTANT] Sichere
HABEN_ENCRYPTION_KEYgetrennt vom Backup, zum Beispiel im Passwortmanager. Haben verschlüsselt damit das ELSTER-Zertifikat samt gespeicherter PIN, die Passwörter für SMTP und IMAP, die Sitzung des Bankabrufs und den Lexoffice-API-Schlüssel (AES-256-GCM). Ohne den Schlüssel sind sie nach einer Wiederherstellung nicht mehr lesbar. Das Backup-Skript sichert ihn nicht mit. Den Wert liest du aus, solange der Container läuft:podman exec haben-app printenv HABEN_ENCRYPTION_KEY.
Konfiguration¶
In ~/.config/haben/haben.env stehen die Werte, die nicht geheim sind. Mindestens die Domain anpassen:
Im Caddyfile dieselbe Domain eintragen (erste Zeile haben.example.de {). Caddy leitet alles an haben-app:3000 weiter und setzt HSTS, X-Content-Type-Options, Referrer-Policy und X-Frame-Options: SAMEORIGIN (nicht DENY, sonst bleiben die eingebetteten Vorschauen leer).
[!NOTE] Leere Zeilen wie
ELSTER_HERSTELLER_ID=gelten als nicht gesetzt. Trag die Hersteller-ID erst ein, wenn du sie hast; sie muss dann genau fünf Ziffern haben.
Starten¶
systemctl --user daemon-reload
systemctl --user start haben-db haben-app haben-caddy
systemctl --user enable --now haben-backup.timer
Die Container-Units starten über [Install] WantedBy=default.target beim nächsten Boot von selbst, sobald daemon-reload gelaufen ist. Logs liest du mit journalctl --user -u haben-app (bzw. haben-db, haben-caddy).
Danach rufst du die Domain im Browser auf und legst das Konto an, siehe Erste Schritte.
Eigener Reverse Proxy¶
Läuft auf dem Server schon ein Reverse Proxy, etwa ein Caddy-, nginx-, Nginx-Proxy-Manager- oder Traefik-Container, braucht Haben keinen eigenen. Dein Proxy leitet die Domain an die App auf Port 3000 weiter. Er muss:
- HTTPS für die Domain bereitstellen. Die Domain ist dieselbe wie
DOMAINin.envbzw.BETTER_AUTH_URL; sonst funktionieren Passkeys nicht. - an
haben-app:3000weiterleiten (Proxy im selben Container-Netz) oder an127.0.0.1:3000(Proxy direkt auf dem Host). - Anfragen bis 250 MB annehmen (Archivdateien bis 100 MB, mehrere Belege auf einmal).
X-Forwarded-Forsetzen. Haben begrenzt Anmeldeversuche je Client-Adresse; ohne den Header zählen alle Anfragen als eine Adresse. Caddy, Nginx Proxy Manager und Traefik setzen ihn von selbst, bei nginx trägst du ihn ein.
Port 3000 der App gehört nicht ins Internet: Dort könnte jeder X-Forwarded-For fälschen, und es gäbe kein HTTPS.
Compose¶
Neben compose.yml eine compose.override.yml ablegen; Compose liest sie automatisch mit, compose.yml bleibt unverändert und lässt sich bei Updates neu herunterladen:
curl -o compose.override.yml https://raw.githubusercontent.com/firsttris/haben/main/deploy/compose.override.example.yml
Darin DEIN_PROXY_NETZ durch das Netz deines Proxy-Containers ersetzen. Das zeigt:
docker inspect DEIN_PROXY_CONTAINER -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'
Die Datei startet den mitgelieferten Caddy nicht mehr und hängt die App zusätzlich in dieses Netz, dort heißt sie haben-app. docker compose up -d startet dann nur db und app.
Läuft dein Proxy direkt auf dem Host statt in einem Container, nimmst du die Variante, die am Ende der Datei auskommentiert steht: Sie veröffentlicht die App nur auf 127.0.0.1:3000, dorthin leitest du weiter.
Quadlets¶
haben-caddy.container, haben-caddy.volume und das Caddyfile nicht ablegen und nur haben-db und haben-app starten. Die App hängt im Netz haben:
- Proxy als Podman-Container: ihn zusätzlich in das Netz
habenhängen. In seiner Quadlet-Datei eine weitere ZeileNetwork=haben.network, sonstpodman network connect haben DEIN_PROXY_CONTAINER. Weiterleiten anhaben-app:3000. - Proxy auf dem Host: in
haben-app.containerdie ZeilePublishPort=127.0.0.1:3000:3000ergänzen und an127.0.0.1:3000weiterleiten.
Beispiele¶
Caddy: Den Block aus deploy/Caddyfile in dein Caddyfile übernehmen, mit deiner Domain, und caddy reload ausführen:
haben.example.de {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
Referrer-Policy same-origin
X-Frame-Options SAMEORIGIN
}
request_body {
max_size 250MB
}
reverse_proxy haben-app:3000
}
nginx: Zertifikat wie für deine anderen Seiten (etwa certbot). Wichtig sind client_max_body_size (Standard ist 1 MB) und die proxy_set_header-Zeilen:
server {
listen 443 ssl;
server_name haben.example.de;
ssl_certificate /etc/letsencrypt/live/haben.example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/haben.example.de/privkey.pem;
client_max_body_size 250M;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy same-origin always;
add_header X-Frame-Options SAMEORIGIN always;
location / {
proxy_pass http://haben-app:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
Nginx Proxy Manager: Neuer Proxy Host mit deiner Domain, Scheme http, Forward Hostname haben-app, Forward Port 3000. Unter SSL ein Let's-Encrypt-Zertifikat anfordern und „Force SSL“ einschalten, unter Advanced client_max_body_size 250M; eintragen.
Traefik: Router für die Domain mit TLS auf den Service haben-app:3000. Traefik begrenzt die Größe von Anfragen nur mit der Middleware buffering (maxRequestBodyBytes); nutzt du sie, dann mit mindestens 250 MB.
Mehrere Instanzen¶
Haben führt genau eine Buchhaltung: ein Unternehmen, ein Konto. Für ein zweites Gewerbe, für Partner oder Familie betreibst du je eine eigene Instanz. Jede hat ihre eigene Datenbank, eigene Belege, eigene Schlüssel und ein eigenes ELSTER-Zertifikat; sichern, umziehen und löschen lässt sich jede für sich. Am einfachsten geht das mit Compose, je Instanz ein Verzeichnis, alle hinter einem gemeinsamen Reverse Proxy.
haben-firma1/
compose.yml # unverändert aus deploy/
compose.override.yml # aus deploy/compose.override.example.yml
.env
haben-firma2/
…
Je Instanz unterscheiden sich drei Dinge:
| Wo | Was | Beispiel |
|---|---|---|
.env |
COMPOSE_PROJECT_NAME: trennt Container, Netz und Volumes der Instanzen (Vorrang vor name: haben in compose.yml) |
COMPOSE_PROJECT_NAME=haben-firma1 |
.env |
eigene DOMAIN, eigenes DB_PASSWORD, eigene Schlüssel (AUTH_SECRET, ENCRYPTION_KEY) |
DOMAIN=firma1.example.de |
compose.override.yml |
eigener Name im Netz des Proxys: den Alias haben-app ersetzen |
aliases: [haben-firma1] |
Der mitgelieferte Caddy belegt Port 80 und 443 und kann deshalb nur einmal laufen. Mit mehreren Instanzen nutzt jede die compose.override.yml aus Eigener Reverse Proxy, und ein gemeinsamer Proxy verteilt die Subdomains, etwa mit Caddy:
firma1.example.de {
request_body {
max_size 250MB
}
reverse_proxy haben-firma1:3000
}
firma2.example.de {
request_body {
max_size 250MB
}
reverse_proxy haben-firma2:3000
}
(Header wie im Beispiel unter Eigener Reverse Proxy.) Starten, aktualisieren und sichern machst du im Verzeichnis der jeweiligen Instanz wie oben unter Docker Compose; die Volumes heißen dann haben-firma1_belege usw. statt haben_belege, das gilt auch für den Backup-Befehl.
Weitere Punkte:
- ERiC lädt jede Instanz einzeln (rund 300 MB). Wer Platz sparen will, entpackt es einmal nach
/opt/ericund bindet es überall mitERIC_HOMEein, siehe ERiC einbinden. - Hersteller-ID: Für deine eigenen Instanzen nimmst du dieselbe ID. Betreibst du eine Instanz für jemand anderen, bist du für diese Person Softwarehersteller nach der ERiC-Lizenz, siehe Hersteller-ID: wer ist Hersteller?. Wer Buchhaltungsdaten für andere hostet, ist zudem Auftragsverarbeiter nach DSGVO.
- Quadlets sind auf eine Instanz ausgelegt (feste Namen wie
haben-app). Für mehrere Instanzen ist Compose einfacher.
Eine Instanz mit mehreren Mandanten gibt es bewusst nicht: Eine gemeinsame Datenbank würde jede Abfrage, die GoBD-Trigger, die Belegablage und das Backup um eine Mandantentrennung erweitern, und ein Fehler darin legte fremde Buchhaltungsdaten offen.
Umgebungsvariablen¶
Die App prüft ihre Variablen in apps/web/src/server/env.ts beim ersten Zugriff. Ein Fehler steht dann im Log von haben-app.
| Variable | Pflicht | Bedeutung | Beispiel |
|---|---|---|---|
DATABASE_URL |
ja | PostgreSQL-Verbindung. Wird auch vom Migrationsschritt beim Start gelesen | postgres://haben:…@haben-db:5432/haben |
BETTER_AUTH_SECRET |
ja | Geheimnis für Sitzungen, mindestens 32 Zeichen | Ausgabe von openssl rand -base64 32 |
BETTER_AUTH_URL |
ja | Öffentliche Adresse, unter der Haben im Browser läuft. Bestimmt die Domain der Passkeys | https://haben.example.de |
HABEN_ENCRYPTION_KEY |
ja | Genau 32 Byte, base64-kodiert. Verschlüsselt ELSTER-Zertifikat und Lexoffice-Schlüssel | Ausgabe von openssl rand -base64 32 |
ERIC_HOME |
nein | Verzeichnis eines von Hand entpackten ERiC-Pakets; hat Vorrang vor dem heruntergeladenen. Ohne beides werden Prüfen und Senden simuliert | /opt/eric |
ERIC_DIR |
nein | Ziel für das von Haben heruntergeladene ERiC. Standard data/eric; im Image /var/lib/haben/eric (Volume) |
/var/lib/haben/eric |
ERIC_AUTO_INSTALL |
nein | ja: fehlt ERiC, lädt der Container es beim Start (Zustimmung zu den Nutzungsbedingungen) |
ja |
ERIC_DOWNLOAD_URL |
nein | Andere Download-Adresse für ERiC, nur zum Testen. Standard https://download.elster.de/download/eric |
http://localhost:4556/eric |
ERIC_LOG_DIR |
nein | Logverzeichnis für ERiC. Ohne Angabe das temporäre Verzeichnis; im Image /var/lib/haben/eric-log |
/var/lib/haben/eric-log |
ERIC_WORKER_PATH |
nein | Pfad zum ERiC-Worker (packages/elster/src/worker.ts). Im Image gesetzt, sonst nicht nötig |
/app/packages/elster/src/worker.ts |
ELSTER_HERSTELLER_ID |
nein | Eigene Hersteller-ID, genau fünf Ziffern. Mit ERiC 43 nötig für Prüfen, Test- und Echtübermittlung; ohne ERiC läuft alles simuliert | 12345 |
DOCUMENTS_DIR |
nein | Ablage der Belegdateien. Standard data/belege (relativ zum Arbeitsverzeichnis); im Image /var/lib/haben/belege |
/var/lib/haben/belege |
ANTHROPIC_API_KEY |
nein | Schaltet die KI-Auslesung von Belegen ein | sk-ant-… |
HABEN_SCHEDULER |
nein | off schaltet die stündlichen Hintergrundjobs ab (wiederkehrende Rechnungen, Kontoabruf), etwa für eine zweite Instanz auf derselben Datenbank |
off |
ENABLE_BANKING_APP_ID |
nein | Application ID der Enable-Banking-Anwendung. Schaltet zusammen mit dem Schlüssel den automatischen Kontoabruf ein | 0b7c3f1e-… |
ENABLE_BANKING_KEY_FILE |
nein | Pfad zum privaten Schlüssel (PEM) der Anwendung. Bei Compose und Quadlets fest /run/secrets/enablebanking.pem |
/run/secrets/enablebanking.pem |
ENABLE_BANKING_KEY |
nein | Alternativ der Schlüssel selbst; Zeilenumbrüche als \n erlaubt |
-----BEGIN PRIVATE KEY-----\n… |
ENABLE_BANKING_API_URL |
nein | Andere Basis-URL der API, nur zum Testen. Standard https://api.enablebanking.com |
http://localhost:4555 |
LEXOFFICE_API_URL |
nein | Andere Basis-URL der Lexware-Office-API. Standard https://api.lexware.io/v1 |
https://api.lexoffice.io/v1 |
PORT |
nein | Port des App-Servers, im Image 3000 |
3000 |
HABEN_EINVOICE_DIR |
nein | Verzeichnis von packages/einvoice (Typst-Vorlage und Schriften für das Rechnungs-PDF). Im Image gesetzt |
/app/packages/einvoice |
Bei Compose setzt compose.yml diese Variablen aus .env (DATABASE_URL aus DB_PASSWORD, BETTER_AUTH_URL aus DOMAIN, BETTER_AUTH_SECRET aus AUTH_SECRET, HABEN_ENCRYPTION_KEY aus ENCRYPTION_KEY). Bei den Quadlets kommen die geheimen Werte aus Podman Secrets, der Rest aus haben.env; für den Datenbank-Container setzt das Quadlet POSTGRES_USER=haben und POSTGRES_DB=haben, das Passwort kommt aus dem Secret haben-db-password.
Datenablage¶
| Was | Wo |
|---|---|
| Buchungen, Rechnungen (PDF und XML), Voranmeldungen mit ERiC-Protokoll, Audit-Log, verschlüsseltes ELSTER-Zertifikat, verschlüsselter Lexoffice-Schlüssel | PostgreSQL, Volume haben-db |
| Belegdateien und aus Lexoffice übernommene Dateien | Volume haben-belege (DOCUMENTS_DIR), abgelegt unter dem SHA-256 der Datei (ab/abcdef…) |
| ERiC, von Haben heruntergeladen | Volume haben-eric (Compose: eric), im Container /var/lib/haben/eric (ERIC_DIR). Lässt sich jederzeit neu laden, nicht im Backup |
| ERiC-Logs | Volume haben-eric-log (Compose: eric-log), im Container /var/lib/haben/eric-log (ERIC_LOG_DIR). Nur zur Fehlersuche, nicht im Backup |
| TLS-Zertifikate von Caddy | Volume haben-caddy |
Belege und Rechnungen müssen nach GoBD zehn Jahre aufbewahrt werden. Beide Volumes mit Buchhaltungsdaten gehören deshalb ins Backup.
Backup¶
Für Compose steht das Backup oben bei Docker Compose. Mit den Quadlets läuft deploy/backup.sh über haben-backup.timer täglich um 03:15 Uhr (Persistent=true: ein verpasster Lauf wird nachgeholt). Das Skript
- erzeugt mit
podman exec haben-db pg_dump -U haben --format=plain habeneinen SQL-Dump, packt ihn mit gzip und prüft ihn: Das Archiv muss sich entpacken lassen und mit der Abschlusszeile vonpg_dumpenden, sonst bricht das Backup mit Fehler ab (set -o pipefail, ein Fehler vonpg_dumpzählt also), - sichert den Dump und das Verzeichnis des Volumes
haben-belegemitpodman unshare restic backup --tag haben. Im rootless Podman gehören die Belegdateien einer Unter-UID des Containers;podman unshareführt restic im Benutzer-Namensraum aus, wo sie lesbar sind und ihre Besitzer behalten, - räumt alte Stände auf: 14 tägliche, 24 monatliche und 11 jährliche Snapshots bleiben (
restic forget --prune).
Die Volumes haben-caddy, haben-eric und haben-eric-log und der Schlüssel HABEN_ENCRYPTION_KEY sind nicht dabei.
Die Unit liest ~/.config/haben/backup.env. Darin stehen die Angaben für restic:
RESTIC_REPOSITORY=sftp:backup@example.de:/srv/restic/haben
RESTIC_PASSWORD_FILE=/home/haben/.config/haben/restic-password
Das Repository legst du einmal mit restic init an. Einen Lauf von Hand startest du mit systemctl --user start haben-backup.service, das Ergebnis steht in journalctl --user -u haben-backup.
Wiederherstellen¶
Für die Quadlets liegt deploy/restore.sh bei. Du hast es beim Ablegen der Dateien neben backup.sh nach ~/.config/haben/ geladen. Es erwartet dieselben Angaben für restic wie das Backup, also RESTIC_REPOSITORY und RESTIC_PASSWORD_FILE aus backup.env in der Umgebung, und einen laufenden Container haben-db:
set -a; . ~/.config/haben/backup.env; set +a
restic snapshots --tag haben # verfügbare Stände anzeigen
~/.config/haben/restore.sh latest # oder eine Snapshot-ID
Das Skript fragt nach, ob Datenbank und Belege ersetzt werden sollen, und macht nur bei der Antwort ja weiter. Dann
- holt es den Snapshot mit
podman unshare restic restorein ein temporäres Verzeichnis und prüft den Dump (lesbar, vollständig), - spielt den Dump mit
psqlin eine neue Datenbankhaben_restoreein; ein Fehler bricht ab (ON_ERROR_STOP), die bisherige Datenbank bleibt dann unverändert, - stoppt
haben-app.service, löscht die Datenbankhabenund benennthaben_restoreinhabenum, - ersetzt den Inhalt des Volumes
haben-belegedurch die Belegdateien aus dem Snapshot, - startet die App wieder.
Danach meldest du dich an und prüfst Stichproben, etwa die letzte Rechnung, den letzten Beleg und die Bankumsätze. Das Skript ersetzt den aktuellen Stand vollständig.
Auf einem frischen System richtest du zuerst die Quadlets wie oben ein, mit denselben Secrets, startest nur haben-db und rufst dann restore.sh auf. HABEN_ENCRYPTION_KEY muss derselbe sein wie vorher, sonst sind ELSTER-Zertifikat, PIN, Mail-Zugänge, Bankabruf und Lexoffice-Schlüssel nicht lesbar.
Von Hand geht es so, ebenfalls auf einem System mit leeren Volumes:
restic snapshots --tag haben
restic restore latest --tag haben --target ~/haben-restore
# Datenbank: nur haben-db starten, die App noch nicht (sie würde sonst Migrationen in die leere DB schreiben)
systemctl --user start haben-db
gunzip -c "$(find ~/haben-restore -name haben.sql.gz)" | podman exec -i haben-db psql -U haben -d haben
# Belegdateien zurück ins Volume, Eigentümer ist der Nutzer node (UID 1000) im Container
ziel="$(podman volume inspect haben-belege --format '{{.Mountpoint}}')"
quelle="$(find ~/haben-restore -type d -path '*haben-belege/_data' | head -n1)"
podman unshare cp -a "$quelle/." "$ziel/"
podman unshare chown -R 1000:1000 "$ziel"
systemctl --user start haben-app haben-caddy
Restic legt Dateien mit ihrem ursprünglichen absoluten Pfad ab, deshalb die find-Aufrufe. Setze vor dem Start der App dasselbe HABEN_ENCRYPTION_KEY wie vorher. Für Docker Compose steht der Weg oben bei Docker Compose. Probiere die Wiederherstellung einmal aus, bevor du dich auf das Backup verlässt.
Updates¶
Mit Compose: docker compose pull && docker compose up -d. Mit den Quadlets:
systemctl --user start haben-backup.service # Stand vor dem Update sichern
podman auto-update # neues Image ziehen und haben-app neu starten
haben-app.container trägt AutoUpdate=registry; mit systemctl --user enable --now podman-auto-update.timer prüft Podman täglich selbst auf neue Images. Willst du Updates von Hand steuern, setze im Quadlet eine feste Version (Image=docker.io/tristanteu/haben:0.1) und ändere sie bewusst. Bei einem selbst gebauten Image: git pull, podman build -t haben -f Containerfile . und systemctl --user restart haben-app.
podman auto-update tauscht nur das Image. Lädst du neue Quadlet-Dateien wie haben-app.container, hole auch die .volume-Dateien, auf die sie verweisen (etwa haben-eric-log.volume), und führe danach systemctl --user daemon-reload aus.
Beim Start führt deploy/entrypoint.sh zuerst src/server/db/migrate.ts aus und wendet alle ausstehenden Migrationen an; erst danach startet der Server. Schlägt eine Migration fehl, startet die App nicht, und der Fehler steht im Log.
ERiC einbinden¶
ERiC ist die Bibliothek der Finanzverwaltung für ELSTER. Sie ist kostenlos, darf aber nicht mit Haben weitergegeben werden. Deshalb lädt Haben sie auf deinen Wunsch direkt von download.elster.de auf deinen Server, nur den Teil für Linux x86_64 (rund 300 MB). Mit dem Download stimmst du den Nutzungsbedingungen von ERiC zu.
| Weg | So geht's |
|---|---|
| Per Knopf | Einstellungen › ELSTER › ERiC: Version prüfen, den Nutzungsbedingungen zustimmen, Herunterladen und einrichten. Der Download läuft im Hintergrund; danach prüft und sendet Haben ohne Neustart über ERiC |
| Beim Start | ERIC_AUTO_INSTALL=ja in .env bzw. haben.env. Fehlt ERiC, lädt der Container es vor dem Start der App; schlägt das fehl, startet die App trotzdem (simuliert) |
| Auf der Kommandozeile | docker compose exec app node --experimental-strip-types /app/packages/elster/src/install-cli.ts --lizenz-akzeptiert [Version] bzw. podman exec haben-app … |
ERiC landet im Volume eric bzw. haben-eric (ERIC_DIR=/var/lib/haben/eric), je Version in einem Unterordner; die Datei AKTUELL zeigt auf die aktive. Eine neue Version ersetzt die alte erst, wenn sie vollständig entpackt ist. Die Version, die Haben vorschlägt, ist die zuletzt bekannte; die aktuelle steht auf der Infoseite. Die Finanzverwaltung stellt alte Versionen nach einiger Zeit ab, und Vordrucke eines neuen Jahres (etwa die Jahreserklärungen) brauchen meist eine neue Version. Dann einfach die neue Versionsnummer eintragen und erneut laden.
Zum Prüfen und Übermitteln brauchst du zusätzlich eine eigene Hersteller-ID; ERiC 43 nimmt die frühere Test-Hersteller-ID nicht mehr an. Vor der ersten Nutzung bestätigst du außerdem unter Einstellungen › ELSTER › ERiC die Datenschutzhinweise der Finanzverwaltung, wie es der ERiC-Lizenzvertrag verlangt. Siehe Erste Schritte, dort auch, wer bei Haben als Hersteller gilt.
Lieber von Hand? Entpacke das Paket auf dem Host nach /opt/eric, sodass /opt/eric/lib/libericapi.so und /opt/eric/lib/plugins/ existieren (bei älteren ERiC-Versionen lib/plugins2/), und binde es ein. ERIC_HOME hat Vorrang vor dem heruntergeladenen ERiC.
| Compose | Quadlet | |
|---|---|---|
| Verzeichnis mounten | in compose.yml die Zeile - /opt/eric:/opt/eric:ro einkommentieren |
in haben-app.container die Zeile Volume=/opt/eric:/opt/eric:ro einkommentieren |
| Variable | ERIC_HOME=/opt/eric in .env |
ERIC_HOME=/opt/eric in haben.env einkommentieren |
| Neu starten | docker compose up -d |
systemctl --user daemon-reload && systemctl --user restart haben-app |
ERiC läuft nie im App-Prozess. Jede Prüfung und jede Übermittlung startet einen kurzlebigen Kindprozess, der die Bibliothek lädt. Ohne ERiC nutzt Haben einen simulierten Client; die Oberfläche zeigt dann „ERiC ist nicht eingerichtet“ an, und nichts geht an das Finanzamt.
Der weitere Ablauf (Zertifikat, Testübermittlung, Hersteller-ID) steht in Erste Schritte.
KI-Auslesung (Anthropic)¶
Optional. Ist ANTHROPIC_API_KEY gesetzt, liest Haben Belege, die keine E-Rechnung sind, mit Claude aus:
printf '%s' 'sk-ant-…' | podman secret create haben-anthropic-key -
# in haben-app.container die Zeile "Secret=haben-anthropic-key,…" einkommentieren
systemctl --user daemon-reload && systemctl --user restart haben-app
Was an Anthropic geht und wann:
- Gesendet wird die Belegdatei selbst (PDF, JPEG, PNG oder WebP, base64-kodiert), zusammen mit einer festen Anweisung und der Liste der Ausgabenkategorien. Weitere Daten aus Haben (Firmendaten, Buchungen, Kontakte) gehen nicht mit.
- Das passiert automatisch direkt nach dem Hochladen, wenn die Datei keine lesbare E-Rechnung (ZUGFeRD, XRechnung) ist, und wenn du auf einem Beleg „Mit KI neu auslesen“ wählst.
- E-Rechnungen liest Haben immer lokal. HEIC-Fotos kann die KI nicht lesen.
- Die Felder werden nur vorbefüllt; gebucht wird erst nach deiner Bestätigung.
Ohne Schlüssel bleibt alles lokal, und du füllst die Felder von Hand aus. Mehr dazu in Belege.
Kontoabruf (Enable Banking)¶
Optional. Haben holt die Umsätze einmal täglich selbst über Enable Banking, einen PSD2-Dienst mit Zugang zu fast allen deutschen Banken (DKB, Sparkassen, Volksbanken, N26 und viele mehr). Ohne Einrichtung bleibt der Import per CSV oder CAMT; beides geht auch nebeneinander.
- Bei Enable Banking ein Konto anlegen und im Kontrollzentrum unter API applications eine Anwendung registrieren.
- Als Redirect URL die Adresse
https://<deine Domain>/api/bank/callbackeintragen. Haben zeigt sie beim Verbinden auch an. - Den Schlüssel im Browser erzeugen lassen. Enable Banking lädt dann
<Application ID>.pemherunter; die Application ID steht auch in der Übersicht. - Für eigene Konten genügt in der Regel der kostenlose eingeschränkte Produktionszugang: Die Anwendung wird aktiviert, indem du im Kontrollzentrum deine eigenen Konten verknüpfst, und darf danach nur diese abrufen. Die aktuellen Bedingungen stehen bei Enable Banking. Zum Ausprobieren gibt es eine Sandbox mit Testbanken.
Schlüssel und ID an Haben übergeben:
# Compose: Datei neben compose.yml legen, Volume-Zeile in compose.yml einkommentieren, ID in .env
cp ~/Downloads/<Application ID>.pem ./enablebanking.pem && chown 1000:1000 enablebanking.pem && chmod 400 enablebanking.pem
echo 'ENABLE_BANKING_APP_ID=<Application ID>' >> .env
docker compose up -d
# Quadlets: Schlüssel als Secret, Zeile "Secret=haben-enablebanking-key,…" in haben-app.container einkommentieren
podman secret create haben-enablebanking-key ~/Downloads/<Application ID>.pem
# in haben.env: ENABLE_BANKING_APP_ID=<Application ID>
systemctl --user daemon-reload && systemctl --user restart haben-app
Danach erscheint unter Bank der Knopf Bank verbinden. Ablauf, Zustimmung und Dubletten stehen in Bankimport und Abgleich.
Was an Enable Banking geht: beim Verbinden die gewählte Bank und die Rückleitungsadresse, danach nur Abrufe von Umsätzen und Kontoständen der freigegebenen Konten. Die Anmeldung bei der Bank geschieht auf deren Seite; Haben sieht weder Zugangsdaten noch TAN. Zahlungen auslösen kann Haben nicht.
Lexware-Office-API¶
Für den Umzug ruft Haben die Public API von Lexware Office ab, standardmäßig unter https://api.lexware.io/v1. LEXOFFICE_API_URL brauchst du nur zum Testen oder falls Lexware die Adresse ändert; laut Code funktioniert auch https://api.lexoffice.io/v1. Den API-Schlüssel trägst du in der Oberfläche ein, nicht in der Umgebung. Ablauf und Voraussetzungen stehen in Umzug aus Lexoffice.
Als App installieren (PWA)¶
Haben bringt ein Web-App-Manifest und einen minimalen Service Worker mit. Der Service Worker speichert bewusst nichts zwischen; die Daten bleiben auf dem Server, offline funktioniert Haben nicht.
- Über „Zum Startbildschirm hinzufügen“ bzw. „App installieren“ im Browser installieren. Das klappt nur über HTTPS.
- Die installierte App hat Kurzbefehle für „Beleg hochladen“ und „Neue Rechnung“.
- Die App meldet sich als Teilen-Ziel an (
/api/belege/teilen): Teilst du auf dem Handy ein PDF, Foto oder E-Rechnungs-XML mit Haben, wird es als Beleg hochgeladen. Höchstens 20 Dateien je Vorgang, je Datei bis 20 MB. Ob das Teilen-Menü Web-Apps anbietet, hängt vom Browser und Betriebssystem ab.
Passkeys und Domain¶
Passkeys sind an den Hostnamen aus BETTER_AUTH_URL gebunden. Ziehst du Haben auf eine andere Domain um, funktionieren die alten Passkeys dort nicht mehr. Melde dich dann mit E-Mail und Passwort an und lege unter Einstellungen neue Passkeys an. BETTER_AUTH_URL muss genau die Adresse sein, die im Browser steht, inklusive https://.
Häufige Probleme¶
Die App zeigt nur einen Fehler, im Log steht ein Zod-Fehler zu einer Variablen. Die Umgebung besteht die Prüfung in env.ts nicht. Häufig: ELSTER_HERSTELLER_ID hat nicht genau fünf Ziffern, HABEN_ENCRYPTION_KEY ist nicht genau 32 Byte base64 oder BETTER_AUTH_SECRET ist kürzer als 32 Zeichen.
haben-app startet nicht, Fehler beim Mount von /opt/eric. Podman bricht ab, wenn das Quellverzeichnis eines Bind-Mounts fehlt. Lege ERiC dort ab oder lass die Volume-Zeile auskommentiert.
„DATABASE_URL fehlt“ oder Verbindungsfehler beim Start. Das Secret haben-database-url fehlt oder das Passwort darin passt nicht zu haben-db-password. Beachte: Postgres übernimmt POSTGRES_PASSWORD nur beim ersten Anlegen des Volumes. Ein später geändertes Secret ändert das Passwort in der Datenbank nicht.
Passkey-Anmeldung schlägt fehl. Meist passt BETTER_AUTH_URL nicht zur aufgerufenen Adresse (anderer Hostname, http statt https, Port). Mit Passwort anmelden geht unabhängig davon. Die Anmeldung ist auf 20 Anfragen pro Minute begrenzt.
„Haben ist bereits eingerichtet.“ Haben hat genau ein Konto. Ein zweites lässt sich nicht anlegen, auch nicht über die API.
Die Oberfläche meldet „ERiC ist nicht eingerichtet“. Unter Einstellungen › ELSTER › ERiC herunterladen. Hast du ERiC von Hand eingebunden, ist ERIC_HOME in der App nicht gesetzt; prüfe .env bzw. haben.env und starte die App neu.
Der ERiC-Download scheitert. Die Meldung steht unter Einstellungen › ELSTER › ERiC. „Gibt es nicht (mehr)“ heißt: Die Version ist zu alt oder vertippt; die aktuelle steht auf der Infoseite. Der Server muss download.elster.de per HTTPS erreichen und für die Dauer des Downloads etwa 600 MB im temporären Verzeichnis frei haben.
Die Vorschau von Belegen, Rechnungen oder Angeboten bleibt leer oder zeigt „Verbindung abgelehnt“. Die Vorschau ist ein eingebettetes Fenster (iframe) auf derselben Adresse. Setzt der Reverse Proxy X-Frame-Options DENY, verbietet das auch diese Einbettung. Stell den Header auf SAMEORIGIN um (so steht es im mitgelieferten Caddyfile und in compose.yml); vor Clickjacking durch fremde Seiten schützt die App selbst mit Content-Security-Policy: frame-ancestors 'self'. Bei Caddy mit einzeln gemounteter Caddyfile (Quadlet) reicht nach dem Bearbeiten caddy reload nicht: Editoren und sed -i legen eine neue Datei an, der Bind-Mount zeigt weiter auf die alte. Starte den Container neu (systemctl --user restart haben-caddy bzw. ohne --user bei rootful Podman).
„download.elster.de nicht erreichbar: …“, obwohl der Server ins Internet kommt. In Klammern steht der Grund. EAI_AGAIN oder ENOTFOUND heißt: Der Container löst keine Namen auf. Prüfen mit podman exec haben-app node -e 'require("dns").promises.lookup("download.elster.de").then(console.log,console.error)' (Compose: docker compose exec app node -e …). Kommt EAI_AGAIN, beantwortet der DNS-Dienst des Podman-Netzes (aardvark-dns) zwar haben-db, leitet externe Namen aber nicht weiter. Das passiert, wenn sich nach dem Start die DNS-Server des Hosts ändern (/etc/resolv.conf, z. B. nach einem Netzwerk-Neustart). Sofort hilft podman network reload --all (rootful) bzw. ein Neustart der Container. Dauerhaft trägst du in haben.network einen festen DNS-Server ein, etwa deinen Router: DNS=192.168.178.1. Danach Container stoppen, podman network rm haben und neu starten. ECONNREFUSED oder ETIMEDOUT deuten auf eine Firewall oder einen Proxy, ein Zertifikatsfehler auf einen TLS-Proxy mit eigener CA. Dieselben Gründe nennt Haben auch, wenn Enable Banking nicht erreichbar ist.
ERiC ist eingerichtet, aber Prüfen oder Senden scheitert sofort. Bei einem von Hand eingebundenen ERiC prüfe, ob unter ERIC_HOME die Dateien lib/libericapi.so und lib/plugins/ (ältere Versionen: lib/plugins2/) liegen und ob das Paket für Linux x86_64 ist. Ein Absturz der Bibliothek beendet nur den Kindprozess; die App zeigt die Meldung an. Details stehen in den ERiC-Logs unter ERIC_LOG_DIR.
Echtübermittlung ist ausgegraut. Es fehlt ELSTER_HERSTELLER_ID. Siehe Erste Schritte.
Belege werden nicht automatisch ausgelesen. Ohne ANTHROPIC_API_KEY liest Haben nur E-Rechnungen. Ist der Schlüssel gesetzt, steht der Grund eines Fehlers am Beleg.
restic meldet beim Sichern der Belege „permission denied“. Bei rootless Podman gehören die Dateien im Volume einer Unter-UID des Hosts, nicht deinem Nutzer. Das aktuelle backup.sh ruft restic deshalb über podman unshare auf. Tritt der Fehler auf, hast du vermutlich noch ein älteres Skript; lade backup.sh neu herunter. Prüfe danach mit restic ls latest, ob die Belege im Snapshot sind.
systemctl --user enable haben-backup.timer findet die Unit nicht. Timer und Service gehören nach ~/.config/systemd/user/, nicht ins Quadlet-Verzeichnis. Danach systemctl --user daemon-reload.
Weiter: Erste Schritte · Entwicklung · Zurück zur Übersicht