Skip to content

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:

yaml
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.

bash
docker compose up -d

Beim 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:

bash
docker compose exec app bun run user -- add \
  --login anna --email anna@example.com --name "Anna Beispiel" --role admin

Ohne --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:

bash
bun run user -- passwd  --login anna
bun run user -- role    --login anna --role lead
bun run user -- disable --login anna

Rollen

RolleSieht
memberdie eigenen Buchungen
leadzusätzlich alles, was auf den eigenen Projekten gebucht ist
adminalles, 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:

caddy
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:

yaml
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:

dockerfile
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/caddy

Im Caddyfile dann:

caddy
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

VariableVorgabeWofür
PORT3000Port des Backends
DATABASE_PATH./attrect.dbPfad zur SQLite-Datei
NODE_ENVproduction schaltet HSTS ein
SECURE_COOKIEStrueauf 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:

  1. Kunde anlegen. Daran hängt der Erfassungstakt — die Schrittweite, in der die Pfeiltasten in den Zeitfeldern springen (Vorgabe: 15 Minuten).
  2. Projekt anlegen, zu diesem Kunden. Hier entscheidet sich, ob Buchungen standardmäßig abrechenbar sind.
  3. Tätigkeiten anlegen — „Entwicklung", „Kommunikation", was bei dir gebraucht wird.
  4. 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.
  5. 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.

bash
docker compose exec app bun run backup -- --out /data/backup-$(date +%F).db
docker compose pull && docker compose up -d

Sichern 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.

bash
# 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 -d

Der 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:

bash
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:

bash
cd backend  && bun install && bun run dev    # API auf :3000
cd frontend && bun install && bun run dev    # Vite auf :5173, /api wird geproxyt

Rechte 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:

bash
docker run --rm -v <volume>:/data alpine chown -R 1000:1000 /data

Was 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.