WorkTime ProHandbuch
Zur Anwendung

Mit Docker betreiben

WorkTime Pro läuft auf der eigenen Infrastruktur. Der Weg dorthin führt über Docker Compose; die vollständige Datei liegt als docker-compose.yml im Repositorium, und .env.example nennt jede Variable mit ihrer Bedeutung.

Dieser Artikel beschreibt den Aufbau. Die Schritt-für-Schritt-Anleitung samt Erstinbetriebnahme steht in der README.md des Repositoriums; sie wird dort gepflegt, damit es nicht zwei Fassungen gibt, die auseinanderlaufen.

Die Dienste

DienstWofürNötig
postgresdie Datenbankja
apidas Backend (Port 4000)ja
webdie Oberfläche (Port 3000)ja
license-serverLizenzen ausstellen und prüfennur beim Hersteller
redisZwischenspeicher und Warteschlangenvorgesehen, noch nicht benutzt
minioObjektspeichervorgesehen, noch nicht benutzt
mailhogPostfach für die Entwicklungnur zur Entwicklung

Zwingend nötig ist nur PostgreSQL. Redis, MinIO und Mailhog sind vorgesehen, werden vom aktuellen Stand aber nicht angesprochen.

Der kleinste sinnvolle Aufbau

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: worktime
      POSTGRES_PASSWORD: worktime
      POSTGRES_DB: worktime
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U worktime']
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://worktime_app:GEHEIM@postgres:5432/worktime?schema=public
      DATABASE_URL_MIGRATIONS: postgresql://worktime:worktime@postgres:5432/worktime?schema=public
      APP_DB_USER: worktime_app
      APP_DB_PASSWORD: GEHEIM
      JWT_SECRET: BITTE-ERZEUGEN-openssl-rand-base64-48
      APP_URL: https://zeit.example.de
      LICENSE_KEY: WTPRO-XXXX
      PORT: '4000'
    ports:
      - '4000:4000'
    depends_on:
      postgres:
        condition: service_healthy

  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    environment:
      NODE_ENV: production
      API_URL: http://api:4000
    ports:
      - '3000:3000'
    depends_on:
      - api

volumes:
  postgres_data:

Die Variablen, auf die es ankommt

VariableBedeutung
JWT_SECRETPflicht, ohne Vorgabewert. Ohne sie startet die API nicht. Erzeugen mit openssl rand -base64 48
APP_URLerlaubte Herkunft für den Browser; im Produktivbetrieb Pflicht
DATABASE_URLZugang der laufenden Anwendung — ohne Sonderrechte
DATABASE_URL_MIGRATIONSZugang für Migrationen, Seed und Richtlinien
APP_DB_USER, APP_DB_PASSWORDdaraus richtet der Entrypoint den Anwendungsbenutzer ein
LICENSE_KEYim Produktivbetrieb Pflicht
NODE_ENVproduction für den Betrieb
ALERT_EMAILwohin die Überwachung meldet

Kein Vorgabewert für JWT_SECRET — ein mitgeliefertes Geheimnis ist öffentlich bekannt und erlaubt jedem, Token für beliebige Konten auszustellen. Im Produktivbetrieb weist die Anwendung außerdem Geheimnisse zurück, die einen bekannten Vorgabewert enthalten oder kürzer als 32 Zeichen sind.

Die Adresse der API wird beim Bauen eingebacken

Next wertet die Weiterleitung /api/* während next build aus. Ein zur Laufzeit gesetztes API_URL bleibt deshalb wirkungslos. Im Dockerfile steht die Adresse als Build-Argument:

docker build --build-arg API_URL=http://meine-api:4000 -f apps/web/Dockerfile .

Das gilt auch für NEXT_PUBLIC_-Variablen: Next trägt sie beim Bauen fest ins Bündel ein, eine Angabe zur Laufzeit erreicht den Browser nie.

Was der Entrypoint der API tut

Beim Start: Migrationen einspielen, Seed ausführen, die Datenbank-Richtlinien anwenden, den Anwendungsbenutzer anlegen oder seine Rechte auffrischen und prüfen, ob dessen Zugang trägt. Schlägt das fehl, bricht er mit einer Zeile ab, statt die Anwendung später an der Anmeldung scheitern zu lassen.

Der Zustandstest

Am api-Dienst hängt ein healthcheck auf /api/v1/ready. docker compose ps zeigt den Container damit als unhealthy, sobald er zwar läuft, aber nichts mehr trägt:

healthcheck:
  test:
    - CMD-SHELL
    - "node -e \"require('http').get('http://127.0.0.1:4000/api/v1/ready',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\""
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 120s

Die Anlaufzeit von 120 Sekunden ist kein Puffer: Migrationen, Seed und Richtlinien laufen vor dem Start, und bis dahin gilt der Container als im Aufbau und nicht als krank.

Die Maschinenkennung

docker-compose.yml bindet /etc/machine-id des Wirts schreibgeschützt in den API-Container ein. Damit bleibt der Hardware-Fingerabdruck über Neustarts hinweg stabil, an dem die Lizenz hängt. Nicht entfernen.

Belegte Ports

Lassen sich in der .env umbiegen: POSTGRES_PORT, API_PORT, WEB_PORT, MINIO_PORT, REDIS_PORT.

Fertige Abbilder für On-Premise-Kunden

Mit einer On-Premise-Lizenz erhalten Sie fertige Abbilder statt des Quellcodes. Den Zugang zur Registry bekommen Sie vom Hersteller.

Beim Kunden liegen nur docker-compose.selfhost.yml, docker-compose.kunde.yml und die .env:

docker login ghcr.io -u <github-konto>   # Token mit read:packages
docker compose -p worktime -f docker-compose.selfhost.yml -f docker-compose.kunde.yml pull
docker compose -p worktime -f docker-compose.selfhost.yml -f docker-compose.kunde.yml up -d

Eine bestimmte Fassung statt der neuesten: WORKTIME_VERSION=1.2.3 in der .env.