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
| Dienst | Wofür | Nötig |
|---|---|---|
postgres | die Datenbank | ja |
api | das Backend (Port 4000) | ja |
web | die Oberfläche (Port 3000) | ja |
license-server | Lizenzen ausstellen und prüfen | nur beim Hersteller |
redis | Zwischenspeicher und Warteschlangen | vorgesehen, noch nicht benutzt |
minio | Objektspeicher | vorgesehen, noch nicht benutzt |
mailhog | Postfach für die Entwicklung | nur 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
| Variable | Bedeutung |
|---|---|
JWT_SECRET | Pflicht, ohne Vorgabewert. Ohne sie startet die API nicht. Erzeugen mit openssl rand -base64 48 |
APP_URL | erlaubte Herkunft für den Browser; im Produktivbetrieb Pflicht |
DATABASE_URL | Zugang der laufenden Anwendung — ohne Sonderrechte |
DATABASE_URL_MIGRATIONS | Zugang für Migrationen, Seed und Richtlinien |
APP_DB_USER, APP_DB_PASSWORD | daraus richtet der Entrypoint den Anwendungsbenutzer ein |
LICENSE_KEY | im Produktivbetrieb Pflicht |
NODE_ENV | production für den Betrieb |
ALERT_EMAIL | wohin 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.