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.

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
- 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. - Linux-Server, VM oder NAS mit mindestens 1 GB RAM (2 GB empfohlen für drei App-Container plus PostgreSQL).
- Für Produktionsbetrieb: Domain oder Subdomain mit DNS-Eintrag auf den Server sowie ein Reverse Proxy für HTTPS (Nginx, Traefik oder Caddy).
- SSH-Zugang zum Host für CLI-Befehle (Artisan-Commands).
- Optional: SMTP-Zugangsdaten für E-Mail-Funktionen (Passwort-Reset, Einladungen).
Eckdaten auf einen Blick
| Parameter | Wert |
|---|---|
| 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-Port | intern (5432, nicht nach außen öffnen) |
| Lizenz | AGPL v3 (kostenfrei, Self-Hosting) |
| Aktuelle Version | v0.14.0 (Juni 2026) |
| Zeitbedarf Einrichtung | ca. 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-storageDer 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.
.env– steuert Docker Compose (Datenbankzugangsdaten, Port, Image-Tag)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=latestErsetze 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=errorWichtige 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-keysDie 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 -dDer 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 appWarte, 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 psErwartetes 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 FoundSchritt 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!')]);
# exitVerifizieren: Ö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=trueOhne 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 -fOhne AUTO_DB_MIGRATE=true musst du nach jedem Update manuell migrieren:
docker compose exec scheduler php artisan migrate --forceVerifizieren: docker compose ps – alle Container zeigen den neuen Image-Digest. Die Solidtime-UI unter Einstellungen bestätigt die neue Version.
Troubleshooting / Typische Fehler
- „500 Internal Server Error" oder „No application encryption key has been specified":
APP_KEYinlaravel.envist leer. Führedocker compose run --rm scheduler php artisan self-host:generate-keysaus und trage alle drei Keys ein. - Leere Seite oder OAuth-Fehler beim Login:
PASSPORT_PRIVATE_KEYoderPASSPORT_PUBLIC_KEYfehlen oder sind ohne Anführungszeichen eingetragen. Die mehrzeiligen RSA-Keys müssen in doppelten Anführungszeichen stehen. - „Table not found" oder fehlende Funktionen nach dem Start: Datenbankmigrationen fehlen. Setze
AUTO_DB_MIGRATE=trueinlaravel.envoder führe manuelldocker compose exec scheduler php artisan migrate --forceaus. - „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. - PDF-Export fehlt oder schlägt fehl: Gotenberg-Container läuft nicht oder
GOTENBERG_URL=http://gotenberg:3000fehlt inlaravel.env. Mitdocker compose ps gotenbergStatus prüfen. - E-Mails werden nicht versendet:
MAIL_*-Variablen nicht konfiguriert. Ohne SMTP funktioniert Passwort-Reset nicht. Mindest-Konfiguration:MAIL_MAILER=smtpplus SMTP-Zugangsdaten eintragen. - Mixed-Content-Fehler oder kaputte Assets hinter Reverse Proxy:
APP_URLohnehttps://oderAPP_FORCE_HTTPS=truefehlt inlaravel.env. Beide Werte prüfen und Stack neu starten. - Verwechslung der beiden Konfigurationsdateien:
.enventhält Docker-Compose-Variablen (DB-Credentials, Port),laravel.enventhält Laravel-App-Konfiguration (APP_KEY, MAIL_*, PASSPORT_*). Variablen in der falschen Datei haben keinen Effekt. - Nach Update fehlen Features oder Fehlermeldungen in Logs: DB-Migration nach Image-Update vergessen. Entweder
AUTO_DB_MIGRATE=truedauerhaft 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 -nFü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
- Docker und Docker Compose auf Linux installieren (Ubuntu/Debian): die Self-Hosting-Grundlage
- Caddy als Reverse Proxy einrichten: Anfänger-Anleitung mit automatischem HTTPS
- MySQL & PostgreSQL Backup automatisieren mit cron: pg_dump, Rotation und Cloud-Sync
- 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)