Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Cloud / Hosting 26.06.2026 · 10 min Lesezeit

Solidtime mit Docker installieren: Modernes Open-Source-Zeiterfassungstool für Agenturen und Freelancer

Solidtime: DSGVO-konformes Open-Source-Zeiterfassungstool mit Projekten, Kunden, abrechenbaren Stunden und Toggl/Clockify-Import – self-hosted per Docker Compose. Anleitung mit allen drei Pflicht-Containern, Key-Generierung und erstem Admin.

Solidtime mit Docker installieren als modernes Open Source Zeiterfassungstool mit Dashboard für Arbeitszeiten, Projekte, Kunden, Berichte und Teamverwaltung.

Wer als Freelancer oder Agentur Projektzeiten, Kunden und abrechenbare Stunden im Blick behalten will, ohne Cloud-Daten an externe Dienste zu übergeben, findet in Solidtime einen ernstzunehmenden Kandidaten. Das Tool (AGPL v3, Stand v0.14.0) bringt Timer, manuelle Einträge, eine Kalenderansicht, eine Timesheet-Bulk-Ansicht, Stundensätze, Rollen und Berichte mit – und dazu einen nativen Import aus Toggl, Clockify und CSV. Mit über 8.700 GitHub-Stars und Releases im Rhythmus von drei bis sechs Wochen ist das Projekt aktiv gepflegt. Diese Anleitung zeigt, wie du Solidtime per docker compose auf einem beliebigen Linux-Host aufsetzen, verifizieren und betreiben kannst.

Voraussetzungen

  1. Docker Engine >= 24.x und Docker Compose Plugin v2 (Befehl: docker compose) auf einem Linux-Host installiert – siehe Docker und Docker Compose auf Linux installieren.
  2. Linux-Server, VM oder NAS mit mindestens 1 GB RAM (2 GB empfohlen für drei App-Container plus PostgreSQL).
  3. Für Produktionsbetrieb: Domain oder Subdomain mit DNS-Eintrag auf den Server sowie ein Reverse Proxy für HTTPS (Nginx, Traefik oder Caddy).
  4. SSH-Zugang zum Host für CLI-Befehle (Artisan-Commands).
  5. Optional: SMTP-Zugangsdaten für E-Mail-Funktionen (Passwort-Reset, Einladungen).

Eckdaten auf einen Blick

ParameterWert
Image (App/Scheduler/Queue)solidtime/solidtime:latest (Docker Hub)
Image (Datenbank)postgres:15
Image (PDF-Export, optional)gotenberg/gotenberg:8
Web-Port (extern)8000 (konfigurierbar via FORWARD_APP_PORT)
Datenbank-Portintern (5432, nicht nach außen öffnen)
LizenzAGPL v3 (kostenfrei, Self-Hosting)
Aktuelle Versionv0.14.0 (Juni 2026)
Zeitbedarf Einrichtungca. 30 Minuten

Schritt 1: Projektordner und Verzeichnisse anlegen

Leg zunächst einen Projektordner an und erstelle die Unterverzeichnisse, die Solidtime auf dem Host braucht. Alle drei App-Container schreiben in dieselben Verzeichnisse – die Berechtigungen müssen daher zur UID 1000 passen, mit der die Container laufen.

mkdir -p /opt/solidtime
cd /opt/solidtime
mkdir -p logs app-storage
chown -R 1000:1000 logs app-storage

Der Ordner logs nimmt Anwendungslogs auf, app-storage persistiert Anwendungsdateien (Uploads, lokaler Dateispeicher). Beide werden von app, scheduler und queue gemeinsam genutzt.

Verifizieren: ls -la /opt/solidtime/ sollte logs und app-storage mit Eigentümer 1000:1000 zeigen.

Schritt 2: Konfigurationsdateien anlegen

Solidtime benötigt zwei separate Konfigurationsdateien – ein häufiger Fehlerquell. Variablen in der falschen Datei haben keinen Effekt.

  1. .env – steuert Docker Compose (Datenbankzugangsdaten, Port, Image-Tag)
  2. laravel.env – steuert die Laravel-Anwendung (App-Key, App-URL, Mail, Passport-Keys)

Datei: .env

# .env – Docker Compose Variablen
APP_DOMAIN=zeiterfassung.beispiel.de
DB_DATABASE=solidtime
DB_USERNAME=solidtime
DB_PASSWORD=einSicheresZufallsPasswort123!
FORWARD_APP_PORT=8000
# DB-Port in Produktion auskommentiert lassen:
# FORWARD_DB_PORT=5432
SOLIDTIME_IMAGE_TAG=latest

Ersetze APP_DOMAIN durch deine Domain und DB_PASSWORD durch ein zufälliges, sicheres Passwort.

Datei: laravel.env (Vorab-Skelett)

Lege die Datei zunächst mit Platzhaltern an – die Keys für APP_KEY, PASSPORT_PRIVATE_KEY und PASSPORT_PUBLIC_KEY werden im nächsten Schritt generiert.

# laravel.env – Laravel-Anwendungskonfiguration
APP_NAME=solidtime
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://zeiterfassung.beispiel.de
APP_FORCE_HTTPS=true
APP_ENABLE_REGISTRATION=false

SUPER_ADMINS=admin@beispiel.de

AUTO_DB_MIGRATE=true

DB_CONNECTION=pgsql
DB_HOST=database
DB_PORT=5432
DB_DATABASE=solidtime
DB_USERNAME=solidtime
DB_PASSWORD=einSicheresZufallsPasswort123!

PASSPORT_PRIVATE_KEY=
PASSPORT_PUBLIC_KEY=

MAIL_MAILER=smtp
MAIL_HOST=smtp.beispiel.de
MAIL_PORT=587
MAIL_USERNAME=no-reply@beispiel.de
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@beispiel.de

GOTENBERG_URL=http://gotenberg:3000
LOG_CHANNEL=stderr
LOG_LEVEL=error

Wichtige Einstellungen: APP_FORCE_HTTPS=true ist zwingend, wenn du Solidtime hinter einem Reverse Proxy mit SSL betreibst – sonst generiert Laravel HTTP-Links und es kommt zu Mixed-Content-Fehlern. APP_ENABLE_REGISTRATION=false verhindert, dass sich Externe registrieren können; Benutzer werden dann per CLI angelegt. AUTO_DB_MIGRATE=true führt Datenbankmigrationen automatisch beim Start aus.

Verifizieren: ls -la /opt/solidtime/ sollte .env, laravel.env, logs/ und app-storage/ zeigen.

Schritt 3: compose.yaml erstellen

Die produktionsreife Compose-Datei orientiert sich am offiziellen self-hosting-examples-Repository. Nicht verwenden: die docker-compose.yml im Haupt-Repository – die ist für die lokale Entwicklung mit Laravel Sail ausgelegt und enthält MinIO, Mailpit, Playwright und weitere Dev-Dienste.

services:
  app:
    restart: always
    image: "solidtime/solidtime:${SOLIDTIME_IMAGE_TAG:-latest}"
    user: "1000:1000"
    ports:
      - "${FORWARD_APP_PORT:-8000}:8000"
    networks:
      - internal
    volumes:
      - "app-storage:/var/www/html/storage"
      - "./logs:/var/www/html/storage/logs"
      - "./app-storage:/var/www/html/storage/app"
    environment:
      CONTAINER_MODE: http
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8000/health-check/up"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    env_file:
      - laravel.env
    depends_on:
      database:
        condition: service_healthy

  scheduler:
    restart: always
    image: "solidtime/solidtime:${SOLIDTIME_IMAGE_TAG:-latest}"
    user: "1000:1000"
    networks:
      - internal
    volumes:
      - "app-storage:/var/www/html/storage"
      - "./logs:/var/www/html/storage/logs"
      - "./app-storage:/var/www/html/storage/app"
    environment:
      CONTAINER_MODE: scheduler
    healthcheck:
      test: ["CMD", "healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 3
    env_file:
      - laravel.env
    depends_on:
      database:
        condition: service_healthy

  queue:
    restart: always
    image: "solidtime/solidtime:${SOLIDTIME_IMAGE_TAG:-latest}"
    user: "1000:1000"
    networks:
      - internal
    volumes:
      - "app-storage:/var/www/html/storage"
      - "./logs:/var/www/html/storage/logs"
      - "./app-storage:/var/www/html/storage/app"
    environment:
      CONTAINER_MODE: worker
      WORKER_COMMAND: "php /var/www/html/artisan queue:work"
    healthcheck:
      test: ["CMD", "healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 3
    env_file:
      - laravel.env
    depends_on:
      database:
        condition: service_healthy

  database:
    restart: always
    image: "postgres:15"
    environment:
      PGPASSWORD: "${DB_PASSWORD:-secret}"
      POSTGRES_DB: "${DB_DATABASE}"
      POSTGRES_USER: "${DB_USERNAME}"
      POSTGRES_PASSWORD: "${DB_PASSWORD:-secret}"
    volumes:
      - "database-storage:/var/lib/postgresql/data"
    networks:
      - internal
    healthcheck:
      test:
        - CMD
        - pg_isready
        - "-q"
        - "-d"
        - "${DB_DATABASE}"
        - "-U"
        - "${DB_USERNAME}"
      interval: 10s
      retries: 3
      timeout: 5s

  gotenberg:
    restart: always
    image: "gotenberg/gotenberg:8"
    networks:
      - internal
    healthcheck:
      test: ["CMD", "curl", "--silent", "--fail", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

networks:
  internal:
    driver: bridge

volumes:
  database-storage:
  app-storage:

Warum drei App-Container? app beantwortet HTTP-Anfragen über Port 8000. scheduler führt geplante Laravel-Artisan-Aufgaben aus – ohne ihn fehlen zeitgesteuerte Features wie automatische Bereinigungen. queue verarbeitet asynchrone Jobs wie E-Mail-Versand und Import-Vorgänge – ohne ihn werden Toggl/Clockify-Importe und Benachrichtigungen nie verarbeitet. Alle drei teilen dieselben Volume-Mounts, damit Dateizugriffe konsistent sind.

Verifizieren: cat /opt/solidtime/compose.yaml – die Datei sollte alle fünf Services (app, scheduler, queue, database, gotenberg) enthalten.

Schritt 4: App-Keys generieren

Vor dem ersten Start müssen drei kryptografische Schlüssel generiert werden: APP_KEY (Laravel-Verschlüsselung) sowie PASSPORT_PRIVATE_KEY und PASSPORT_PUBLIC_KEY (OAuth für Desktop-Client und Browser-Extensions). Ohne diese Keys startet die Anwendung entweder gar nicht oder liefert einen „500 Internal Server Error" mit der Meldung „No application encryption key has been specified".

Starte dazu zunächst nur die Datenbank, dann führe den Schlüsselgenerator aus:

cd /opt/solidtime
docker compose up -d database
# Warte ~10 Sekunden bis PostgreSQL healthy ist, dann:
docker compose run --rm scheduler php artisan self-host:generate-keys

Die Ausgabe enthält drei Werte in diesem Format:

APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==

PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEow...
-----END RSA PRIVATE KEY-----"

PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIj...
-----END PUBLIC KEY-----"

Trage diese Werte exakt so in laravel.env ein. Die mehrzeiligen Passport-Keys müssen in doppelten Anführungszeichen stehen. Anschließend DB_PASSWORD in laravel.env kontrollieren – muss mit dem Wert in .env übereinstimmen.

Verifizieren: grep APP_KEY /opt/solidtime/laravel.env sollte einen nicht-leeren base64:...-Wert zeigen. grep PASSPORT_PRIVATE /opt/solidtime/laravel.env sollte den RSA-Schlüssel ausgeben.

Schritt 5: Stack starten

Jetzt startest du den vollständigen Stack:

cd /opt/solidtime
docker compose up -d

Der erste Start dauert einige Minuten – PostgreSQL muss initialisiert werden, die App-Container laden das Image und AUTO_DB_MIGRATE=true führt alle Datenbankmigrationen automatisch aus. Beobachte den Startvorgang:

docker compose logs -f app

Warte, bis die Logs zeigen, dass der Server auf Port 8000 lauscht. Typisch ist eine Meldung wie Server running on http://0.0.0.0:8000 (FrankenPHP/Octane).

Verifizieren:

docker compose ps

Erwartetes Ergebnis – alle fünf Container mit Status running bzw. healthy:

NAME                    IMAGE                           STATUS
solidtime-app-1         solidtime/solidtime:latest      Up (healthy)
solidtime-scheduler-1   solidtime/solidtime:latest      Up (healthy)
solidtime-queue-1       solidtime/solidtime:latest      Up (healthy)
solidtime-database-1    postgres:15                     Up (healthy)
solidtime-gotenberg-1   gotenberg/gotenberg:8           Up (healthy)

Anschließend HTTP-Erreichbarkeit prüfen:

curl -I http://localhost:8000
# Erwartete Antwort: HTTP/1.1 200 OK oder HTTP/1.1 302 Found

Schritt 6: Ersten Admin-Benutzer anlegen

Solidtime hat keine web-basierte Ersteinrichtung. Der erste Benutzer wird per Artisan-Befehl angelegt:

cd /opt/solidtime
docker compose exec scheduler php artisan admin:user:create 'Vorname Nachname' 'admin@beispiel.de'

Die E-Mail-Adresse muss mit dem Wert in SUPER_ADMINS in laravel.env übereinstimmen, damit Super-Admin-Rechte greifen. Danach setzt du ein Passwort über die Passwort-vergessen-Funktion im Browser – oder falls E-Mail noch nicht konfiguriert ist, direkt per Artisan im Tinker-REPL:

docker compose exec scheduler php artisan tinker
# Im Tinker-Prompt:
# \App\Models\User::where('email','admin@beispiel.de')->first()->update(['password' => bcrypt('MeinPasswort!')]);
# exit

Verifizieren: Öffne http://localhost:8000 (oder deine Domain) im Browser. Die Login-Seite von Solidtime sollte erscheinen. Nach dem Login siehst du das Dashboard mit dem leeren Workspace.

Schritt 7: Reverse Proxy und HTTPS (Produktion)

Für den Produktionsbetrieb schaltest du einen Reverse Proxy vor, der Port 80/443 auf den Container-Port 8000 weiterleitet. Eine ausführliche Anleitung findest du unter Caddy als Reverse Proxy einrichten oder Traefik als Docker-Reverse-Proxy. Das offizielle self-hosting-examples-Repository enthält zudem ein fertiges Traefik-Beispiel.

Stelle sicher, dass in laravel.env folgende Werte korrekt gesetzt sind, bevor du den Stack neu startest:

APP_URL=https://zeiterfassung.beispiel.de
APP_FORCE_HTTPS=true

Ohne APP_FORCE_HTTPS=true generiert Laravel HTTP-Links, was zu Mixed-Content-Fehlern und kaputten Assets führt.

Verifizieren: curl -I https://zeiterfassung.beispiel.de sollte HTTP/2 200 zurückgeben. Im Browser: kein Zertifikatsfehler, alle Assets laden korrekt.

Schritt 8: Updates einspielen

Da AUTO_DB_MIGRATE=true gesetzt ist, reichen zwei Befehle für ein Update:

cd /opt/solidtime
docker compose pull
docker compose up -d
# Optional: alte Images aufräumen
docker image prune -f

Ohne AUTO_DB_MIGRATE=true musst du nach jedem Update manuell migrieren:

docker compose exec scheduler php artisan migrate --force

Verifizieren: docker compose ps – alle Container zeigen den neuen Image-Digest. Die Solidtime-UI unter Einstellungen bestätigt die neue Version.

Troubleshooting / Typische Fehler

  1. „500 Internal Server Error" oder „No application encryption key has been specified": APP_KEY in laravel.env ist leer. Führe docker compose run --rm scheduler php artisan self-host:generate-keys aus und trage alle drei Keys ein.
  2. Leere Seite oder OAuth-Fehler beim Login: PASSPORT_PRIVATE_KEY oder PASSPORT_PUBLIC_KEY fehlen oder sind ohne Anführungszeichen eingetragen. Die mehrzeiligen RSA-Keys müssen in doppelten Anführungszeichen stehen.
  3. „Table not found" oder fehlende Funktionen nach dem Start: Datenbankmigrationen fehlen. Setze AUTO_DB_MIGRATE=true in laravel.env oder führe manuell docker compose exec scheduler php artisan migrate --force aus.
  4. „Permission denied" beim Schreiben in Storage-Verzeichnisse: Host-Verzeichnisse gehören nicht UID 1000. Fix: chown -R 1000:1000 /opt/solidtime/logs /opt/solidtime/app-storage.
  5. PDF-Export fehlt oder schlägt fehl: Gotenberg-Container läuft nicht oder GOTENBERG_URL=http://gotenberg:3000 fehlt in laravel.env. Mit docker compose ps gotenberg Status prüfen.
  6. E-Mails werden nicht versendet: MAIL_*-Variablen nicht konfiguriert. Ohne SMTP funktioniert Passwort-Reset nicht. Mindest-Konfiguration: MAIL_MAILER=smtp plus SMTP-Zugangsdaten eintragen.
  7. Mixed-Content-Fehler oder kaputte Assets hinter Reverse Proxy: APP_URL ohne https:// oder APP_FORCE_HTTPS=true fehlt in laravel.env. Beide Werte prüfen und Stack neu starten.
  8. Verwechslung der beiden Konfigurationsdateien: .env enthält Docker-Compose-Variablen (DB-Credentials, Port), laravel.env enthält Laravel-App-Konfiguration (APP_KEY, MAIL_*, PASSPORT_*). Variablen in der falschen Datei haben keinen Effekt.
  9. Nach Update fehlen Features oder Fehlermeldungen in Logs: DB-Migration nach Image-Update vergessen. Entweder AUTO_DB_MIGRATE=true dauerhaft setzen oder nach jedem Update manuell migrieren.

Häufige Fragen

Kann ich Solidtime ohne Reverse Proxy betreiben?

Ja – für Tests und den Einsatz im internen Netzwerk reicht Port 8000 direkt. Für Produktionsbetrieb mit HTTPS und öffentlicher Erreichbarkeit wird ein Reverse Proxy dringend empfohlen. Das Repository solidtime-io/self-hosting-examples enthält ein fertiges Traefik-Beispiel inklusive automatischer SSL-Zertifikatsverwaltung.

Wie importiere ich Daten aus Toggl oder Clockify?

Solidtime bietet einen nativen Import direkt in der Web-UI: Einstellungen → Import. Unterstützt werden Toggl, Clockify und generische Zeiteintrags-CSV-Dateien. Der Import wird asynchron über den Queue-Worker verarbeitet – der queue-Container muss also laufen. Der Status des Import-Jobs ist in der UI einsehbar.

Wie richte ich den Desktop-Client oder Browser-Extensions ein?

Für den Desktop-Client muss ein dedizierter OAuth-Client angelegt werden:

docker compose exec scheduler php artisan passport:client \
  --name=desktop \
  --redirect_uri=solidtime://oauth/callback \
  --public -n

Für persönlichen API-Zugriff (z. B. Browser-Extension):

docker compose exec scheduler php artisan passport:client \
  --personal --name='API'

Was ist der Unterschied zwischen Community Self-Hosting und On-Premise Business?

Das Community Self-Hosting ist kostenlos (AGPL v3) und umfasst alle Kernfunktionen. Das On-Premise Business Angebot richtet sich an Unternehmen, die professionellen Support, SLA oder zusätzliche Enterprise-Features benötigen. Für die meisten DACH-Agenturen und Freelancer ist die Community-Variante vollständig ausreichend.

Wie sichere ich die Solidtime-Daten?

Das benannte Docker-Volume database-storage enthält alle PostgreSQL-Daten. Eine solide Backup-Strategie für Docker-Volumes und PostgreSQL-Dumps ist in der Anleitung MySQL & PostgreSQL Backup automatisieren beschrieben. Ergänzend solltest du das Verzeichnis /opt/solidtime/app-storage sichern, da dort Uploads liegen.

Kann ich die Image-Version fest einpinnen?

Ja – setze in .env den Wert SOLIDTIME_IMAGE_TAG=v0.14.0 (oder die gewünschte Version). Für Produktionsumgebungen ist ein festes Tag empfehlenswert, damit Updates kontrolliert eingespielt werden. Aktuelle Releases findest du auf der Releases-Seite des Haupt-Repositories.

Fazit

Solidtime ist ein gut durchdachtes Open-Source-Zeiterfassungstool, das den Kernbedarf von Agenturen und Freelancern – Projekte, Kunden, abrechenbare Stunden, Import aus bestehenden Tools – ohne Cloud-Abhängigkeit abdeckt. Die Drei-Container-Architektur (app, scheduler, queue) ist für Docker-erfahrene Nutzer schnell aufgesetzt, erfordert aber Sorgfalt bei der Konfiguration: die zwei getrennten Dateien .env und laravel.env sowie die Key-Generierung sind die häufigsten Stolpersteine. Mit AUTO_DB_MIGRATE=true und einem sauberen Update-Workflow bleibt der Betrieb langfristig wartungsarm. Wer einen DSGVO-konformen Toggl- oder Clockify-Ersatz sucht und bereits Docker-Infrastruktur betreibt, kommt mit Solidtime in etwa 30 Minuten produktiv.

Weiterführende Anleitungen und Quellen

  1. Docker und Docker Compose auf Linux installieren (Ubuntu/Debian): die Self-Hosting-Grundlage
  2. Caddy als Reverse Proxy einrichten: Anfänger-Anleitung mit automatischem HTTPS
  3. MySQL & PostgreSQL Backup automatisieren mit cron: pg_dump, Rotation und Cloud-Sync
  4. Docker Compose absichern: Secrets, Healthchecks, Non-Root und Read-Only für den Produktivbetrieb

Quellen: Solidtime Self-Hosting Docker Guide (offizielle Dokumentation) · solidtime-io/self-hosting-examples auf GitHub · Solidtime Releases (GitHub)

Passende Anleitungen auf S-EDV

  1. PostgreSQL schließt elf Sicherheitslücken in den Versionen 14 bis 18
  2. netcup Local Block Storage bestellen, einrichten und unter Linux einbinden
  3. CVE-2022-0492: CISA warnt vor aktiv ausgenutztem Container-Escape im Linux-Kerne