attrect selbst betreiben
attrect ist eine Anwendung, die du auf deinem eigenen Server betreibst. Es gibt keine gehostete Variante zum Mitbenutzen. Der Quellcode steht unter der MIT-Lizenz.
Ein Container enthält alles: Bun liefert das gebaute Frontend aus und beantwortet die API auf Port 3000. Die Daten liegen in einer einzigen SQLite-Datei.
Zum Selberbauen brauchst du zwei kommerzielle Lizenzen
Das Frontend benutzt FontAwesome Pro und WebAwesome Pro. Beide sind kostenpflichtig und liegen in privaten npm-Registries. Ohne Zugang zu diesen Paketen lässt sich das Frontend nicht aus dem Quellcode bauen — der Rest der Anwendung ist davon nicht betroffen, das Backend hat keine privaten Abhängigkeiten.
Wenn du ein fertiges Image verwendest, brauchst du die Lizenzen nicht: die Pakete stecken dann bereits im gebauten Frontend.
Was du brauchst
- Docker mit Compose
- Einen Reverse Proxy, der TLS beendet. Der Container spricht einfaches HTTP auf Port 3000 und kümmert sich nicht um Zertifikate — siehe Reverse Proxy mit Caddy.
- Ein Volume für die Datenbank. Ohne eines liegen die Daten in der Schreibschicht des Containers und sind mit ihm weg.
Einrichten
Lege eine compose.yaml an:
services:
app:
image: registry.cepharum.de/attrect:latest
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_PATH: /data/attrect.db
SECURE_COOKIES: "true"
volumes:
- db-data:/data
restart: unless-stopped
volumes:
db-data:Das Image ist öffentlich lesbar: docker compose pull braucht keine Anmeldung an der Registry. Statt :latest besser den kurzen Commit-Hash eines Stands einsetzen — dann ist benennbar, was läuft.
docker compose up -dBeim Start legt die Anwendung das Datenbankschema an und führt alle Migrationen aus. /api/health antwortet mit ok, sobald sie damit fertig ist; das Image erklärt diesen Aufruf als eigenen HEALTHCHECK, docker compose ps zeigt also Gesundheit und nicht bloß einen laufenden Prozess.
Die erste Administration anlegen
Nach einer frischen Installation existiert kein einziger Benutzer, und niemand kann sich anmelden, bis einer da ist:
docker compose exec app bun run user -- add \
--login anna --email anna@example.com --name "Anna Beispiel" --role adminOhne --password wird ein zufälliges Passwort erzeugt und einmal ausgegeben. Notiere es, bevor du das Terminal schließt.
Weitere Benutzer legt die Administration danach in der Anwendung an. Auf der Kommandozeile geht das ebenfalls:
bun run user -- passwd --login anna
bun run user -- role --login anna --role lead
bun run user -- disable --login annaRollen
| Rolle | Sieht |
|---|---|
member | die eigenen Buchungen |
lead | zusätzlich alles, was auf den eigenen Projekten gebucht ist |
admin | alles, und verwaltet Benutzer, Kunden, Projekte und Sperren |
TLS ist nicht optional
Das Sitzungs-Cookie ist als Secure markiert. Über einfaches HTTP verwirft der Browser es, und dann kann sich niemand anmelden — es sieht aus wie ein falsches Passwort, ist aber der fehlende TLS-Abschluss. Nur zum lokalen Ausprobieren hinter einem HTTP-Proxy setzt du SECURE_COOKIES: "false".
Reverse Proxy mit Caddy
Caddy ist hier die Empfehlung: er holt und erneuert die Zertifikate von selbst, und die ganze Konfiguration ist der Block unten. Jeder andere Proxy geht auch — was attrect braucht, ist TLS-Abschluss und eine Weiterleitung auf Port 3000.
Caddyfile:
attrect.example.com {
reverse_proxy app:3000
}Damit lässt du attrect den Port nicht mehr am Host veröffentlichen — nur Caddy hört nach außen:
services:
app:
image: registry.cepharum.de/attrect:latest
expose:
- "3000"
environment:
NODE_ENV: production
DATABASE_PATH: /data/attrect.db
SECURE_COOKIES: "true"
volumes:
- db-data:/data
restart: unless-stopped
caddy:
image: caddy:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
restart: unless-stopped
volumes:
db-data:
caddy-data:
caddy-config:Das Volume für /data bei Caddy ist keine Kür
Dort liegen die Zertifikate und der Kontoschlüssel. Fehlt es, holt Caddy nach jedem docker compose up neue — und Let's Encrypt begrenzt das pro Domain und Woche. Nach einigen Neustarts steht die Seite dann ohne gültiges Zertifikat da.
Port 80 muss offen bleiben, auch wenn dort nichts bedient wird: darüber läuft die Prüfung, mit der Caddy die Zertifikate bekommt. Und die Domain muss auf den Host zeigen, bevor du startest.
Anmeldeversuche begrenzen
Caddy kann das nicht von Haus aus — die Zählung steckt in einem Plugin, und Plugins werden bei Caddy einkompiliert. Das ist ein zweistufiges Image:
FROM caddy:builder AS builder
RUN xcaddy build --with github.com/mholt/caddy-ratelimit
FROM caddy:alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddyIm Caddyfile dann:
attrect.example.com {
rate_limit {
zone anmeldung {
match {
path /api/auth/login /api/auth/password
}
key {http.request.remote.host}
events 10
window 1m
}
}
reverse_proxy app:3000
}Zehn Versuche je Minute und IP reichen für jeden, der sich vertippt, und machen das geduldige Durchprobieren von Passwörtern unbrauchbar. /api/auth/password ist mit dabei, weil dort ebenso ein Passwort geprüft wird.
Und im Compose-Block das image: caddy:alpine durch ein build: . auf dieses Dockerfile ersetzen.
Umgebungsvariablen
| Variable | Vorgabe | Wofür |
|---|---|---|
PORT | 3000 | Port des Backends |
DATABASE_PATH | ./attrect.db | Pfad zur SQLite-Datei |
NODE_ENV | — | production schaltet HSTS ein |
SECURE_COOKIES | true | auf false nur zum Testen ohne TLS |
Erste Schritte in der Anwendung
Erfassen lässt sich erst, wenn es etwas zu bebuchen gibt. Der Reihenfolge nach:
- Kunde anlegen. Daran hängt der Erfassungstakt — die Schrittweite, in der die Pfeiltasten in den Zeitfeldern springen (Vorgabe: 15 Minuten).
- Projekt anlegen, zu diesem Kunden. Hier entscheidet sich, ob Buchungen standardmäßig abrechenbar sind.
- Tätigkeiten anlegen — „Entwicklung", „Kommunikation", was bei dir gebraucht wird.
- Optional: Stundensätze, Budgets je Zeitraum und den Kalender der Tage, an denen nicht gearbeitet wird. Ohne Feiertage und Abwesenheiten rechnet die Prognose mit zu vielen Arbeitstagen. Die gesetzlichen Feiertage eines Jahres holt „Feiertage importieren" unter Feiertage und Abwesenheiten — einmal ist der Sitz des Unternehmens zu wählen, denn er entscheidet über die regionalen Feiertage; die Auswahl bleibt für die folgenden Jahre gespeichert. Bereits arbeitsfreie Tage bleiben unverändert, der Import lässt sich also gefahrlos wiederholen.
- Loslegen — siehe Zeit erfassen.
Aktualisieren
Migrationen laufen beim Start, jede in einer Transaktion. Eine fehlgeschlagene hinterlässt also keine halb migrierte Datenbank — aber sie hinterlässt den alten Container als gelöscht. Deshalb kommt die Sicherung zuerst. Migrationen gehen nur vorwärts; es gibt keinen Rückweg, ein Rollback heißt Wiederherstellen.
docker compose exec app bun run backup -- --out /data/backup-$(date +%F).db
docker compose pull && docker compose up -dSichern und Wiederherstellen
Die Sicherung benutzt VACUUM INTO, der Schnappschuss ist also auch konsistent, während Anfragen bedient werden. Kopiere nicht die .db-Datei — im WAL-Modus wäre die Kopie unvollständig.
# Schnappschuss, und vom Host wegkopieren
docker compose exec app bun run backup -- --out /data/backup-$(date +%F).db
docker compose cp app:/data/backup-$(date +%F).db ./
# Wiederherstellen: anhalten, Datei an ihren Platz, starten
docker compose down
docker compose cp ./backup-2026-08-01.db app:/data/attrect.db
docker compose up -dDer Zielname muss der aus DATABASE_PATH sein — steht dort etwas anderes als attrect.db, gehört die Datei auch dorthin. docker compose exec app ls /data zeigt, wie sie heißt.
Ein Schnappschuss am Tag, außerhalb des Hosts aufbewahrt, ist die ganze Sicherungsstrategie, die das hier braucht. Es ist eine Datei.
Aus dem Quellcode bauen
Nur mit den beiden Lizenzen von oben. Die privaten Registries werden über eine .npmrc im Projektstamm aufgelöst, die als BuildKit-Secret in den Build geht statt in ein Image zu wandern:
git clone https://gitlab.com/cepharum-foss/attrect.git
cd attrect
# .npmrc mit den Zugangsdaten für @fortawesome und @web.awesome.me anlegen
DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=.npmrc -t attrect .Für die Entwicklung laufen Backend und Frontend getrennt:
cd backend && bun install && bun run dev # API auf :3000
cd frontend && bun install && bun run dev # Vite auf :5173, /api wird geproxytRechte auf dem Volume
Der Container läuft als unprivilegierter Benutzer bun (uid 1000), und das Image gibt /data diesem Benutzer. Ein Volume, das aus diesem Image entsteht, gehört damit dem Richtigen — du musst hier nichts tun.
Bringst du ein Volume von woanders mit, muss uid 1000 darin schreiben dürfen. Sonst lautet der Fehler beim ersten Schreiben SQLITE_READONLY, und die Rechte lassen sich einmalig setzen:
docker run --rm -v <volume>:/data alpine chown -R 1000:1000 /dataWas attrect selbst nicht tut
Anmeldeversuche zählt die Anwendung nicht. Sitzungen liegen serverseitig und Passwörter sind gehasht, aber gegen das geduldige Durchprobieren hilft nur, die Versuche zu begrenzen — und der Ort dafür ist der Proxy, der ohnehin davorsteht. Wie das mit Caddy geht, steht oben.
Datenschutz
Die Anwendung lädt nichts von fremden Servern — keine Web-Fonts, keine CDNs, kein Tracking. Ein Cookie-Banner ist damit unnötig: das Sitzungs-Cookie und die gespeicherten Anzeigeeinstellungen (Sprache, Erscheinungsbild) sind technisch erforderlich beziehungsweise vom Nutzer selbst gesetzt.