Solidtime mit Docker installieren: Modernes Open-Source-Zeiterfassungstool für Agenturen und Freelancer
Solidtime ist ein Open-Source-Zeiterfassungstool mit Projekten, Kunden, abrechenbaren Stunden und Import aus Toggl und Clockify. Die Anleitung zeigt die Installation per Docker Compose mit festem Image-Tag, Key-Generierung und erstem Admin.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Solidtime erfasst Projektzeiten, Kunden und abrechenbare Stunden für Freelancer und Agenturen, ohne Daten an externe Cloud-Dienste zu übergeben. Das Tool (AGPL v3, Stand Version 0.22.0, Oktober 2026) bringt Timer, manuelle Einträge, Kalenderansicht, Timesheet-Bulk-Ansicht, Stundensätze, Rollen und Berichte mit, dazu einen Import aus Toggl, Clockify und CSV. Diese Anleitung zeigt, wie Sie Solidtime per docker compose auf einem Linux-Host aufsetzen, prüfen und betreiben.
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:0.22.0 (Docker Hub, Tags ohne „v“) |
| Image (Datenbank) | postgres:15 |
| Image (PDF-Export, optional) | gotenberg/gotenberg:8 |
| Web-Port (extern) | 8000, nur an 127.0.0.1 gebunden (konfigurierbar via FORWARD_APP_PORT) |
| Datenbank-Port | intern (5432, nicht nach außen öffnen) |
| Lizenz | AGPL v3 (kostenfrei, Self-Hosting) |
| Aktuelle Version | 0.22.0 (Oktober 2026) |
| Zeitbedarf Einrichtung | ca. 30 Minuten |
Schritt 1: Projektordner und Verzeichnisse anlegen
Legen Sie zunächst einen Projektordner und die Unterverzeichnisse an, 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-storagelogs nimmt Anwendungslogs auf, app-storage Uploads und lokale Dateien.
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, eine häufige Fehlerquelle. 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=0.22.0Ersetzen Sie APP_DOMAIN durch Ihre Domain und DB_PASSWORD durch ein zufälliges Passwort (z. B. openssl rand -hex 24).
Datei: laravel.env (Vorab-Skelett)
Legen Sie 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
# Wert aus dem offiziellen Beispiel; nur sicher, weil Port 8000 an 127.0.0.1 gebunden ist
TRUSTED_PROXIES=0.0.0.0/0,2000:0:0:0:0:0:0:0/3
# on | invite-only | off
APP_ENABLE_REGISTRATION=off
SUPER_ADMINS=admin@beispiel.de
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 Sie Solidtime hinter einem Reverse Proxy mit TLS betreiben; TRUSTED_PROXIES sorgt dafür, dass Solidtime die Weiterleitungs-Header des Proxys akzeptiert, sonst generiert Laravel HTTP-Links und es kommt zu Mixed-Content-Fehlern. APP_ENABLE_REGISTRATION=off verhindert Selbstregistrierungen; Benutzer legen Sie per CLI oder im Super-Admin-Panel an (invite-only erlaubt nur eingeladene Adressen).
ls -la /opt/solidtime/ sollte .env, laravel.env, logs/ und app-storage/ zeigen.
Schritt 3: compose.yaml erstellen
Die Compose-Datei orientiert sich am offiziellen self-hosting-examples-Repository. Nicht verwenden: die docker-compose.yml im Haupt-Repository, sie 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:
- "127.0.0.1:${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
AUTO_DB_MIGRATE: "true" # Migrationen nur im app-Container
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:app beantwortet HTTP-Anfragen, scheduler führt geplante Aufgaben aus, queue verarbeitet asynchrone Jobs wie E-Mails und Importe. Alle drei teilen dieselben Volume-Mounts. Abweichend vom Beispiel bindet diese Datei Port 8000 nur an 127.0.0.1, damit Solidtime ausschließlich über den Reverse Proxy erreichbar ist, und setzt AUTO_DB_MIGRATE nur im app-Container, damit nicht drei Container gleichzeitig migrieren.
docker compose config --services listet app, scheduler, queue, database und gotenberg; eine Fehlermeldung weist auf einen YAML- oder Variablenfehler hin.
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 liefert die Anwendung einen Fehler 500.
Führen Sie den Schlüsselgenerator in einem Einmal-Container aus:
cd /opt/solidtime
docker compose run --rm scheduler php artisan self-host:generate-keysDie Ausgabe enthält drei Werte:
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-----"Tragen Sie diese Werte exakt so in laravel.env ein. Die mehrzeiligen Passport-Keys müssen in doppelten Anführungszeichen stehen. Kontrollieren Sie anschließend, dass DB_PASSWORD in laravel.env mit dem Wert in .env übereinstimmt.
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
Starten Sie 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 im app-Container die Datenbankmigrationen aus. Startvorgang beobachten:
docker compose logs -f appWarten Sie, bis keine Migrationsmeldungen mehr folgen und der Healthcheck des app-Containers greift (Strg+C beendet die Logausgabe).
docker compose psErwartetes Ergebnis, alle fünf Container mit Status running bzw. healthy:
NAME IMAGE STATUS
solidtime-app-1 solidtime/solidtime:0.22.0 Up (healthy)
solidtime-scheduler-1 solidtime/solidtime:0.22.0 Up (healthy)
solidtime-queue-1 solidtime/solidtime:0.22.0 Up (healthy)
solidtime-database-1 postgres:15 Up (healthy)
solidtime-gotenberg-1 gotenberg/gotenberg:8 Up (healthy)Anschließend HTTP-Erreichbarkeit prüfen:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/health-check/up
# Erwartete Antwort: 200Andere Seiten wie /login beantwortet Solidtime über 127.0.0.1 mit 400 und der Seite „Untrusted host“, weil der Host nicht zu APP_URL passt. Das ist kein Fehler; über die Domain aus APP_URL (also hinter dem Reverse Proxy) erscheint die Anmeldeseite.
Schritt 6: Ersten Admin-Benutzer anlegen
Den ersten Benutzer legen Sie per Artisan-Befehl an:
cd /opt/solidtime
docker compose exec scheduler php artisan admin:user:create 'Vorname Nachname' 'admin@beispiel.de' --verify-email --ask-for-passwordDie E-Mail-Adresse muss mit dem Wert in SUPER_ADMINS in laravel.env übereinstimmen, damit Super-Admin-Rechte greifen. --ask-for-password fragt das Passwort interaktiv ab; ohne die Option erzeugt der Befehl ein Zufallspasswort und gibt es aus. --verify-email bestätigt die Adresse, auch wenn noch kein SMTP eingerichtet ist. Falls Sie ein Passwort nachträglich setzen müssen, geht das 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Öffnen Sie Ihre Domain im Browser (bis zum Reverse Proxy per SSH-Tunnel ssh -L 8000:127.0.0.1:8000 server und http://localhost:8000). Die Login-Seite erscheint, nach dem Login das Dashboard mit leerem Workspace.
Schritt 7: Reverse Proxy und HTTPS (Produktion)
Für den Produktionsbetrieb schalten Sie einen Reverse Proxy vor, der Port 443 auf 127.0.0.1:8000 weiterleitet. Anleitungen: Caddy als Reverse Proxy einrichten oder Traefik als Docker-Reverse-Proxy. Das offizielle self-hosting-examples-Repository enthält zudem ein fertiges Traefik-Beispiel.
Prüfen Sie in laravel.env diese Werte, bevor Sie den Stack neu starten:
APP_URL=https://zeiterfassung.beispiel.de
APP_FORCE_HTTPS=truecurl -I https://zeiterfassung.beispiel.de sollte HTTP/2 200 zurückgeben. Im Browser: kein Zertifikatsfehler, alle Assets laden korrekt.
Schritt 8: Updates einspielen
Lesen Sie vor dem Update die Release Notes, sichern Sie die Datenbank und setzen Sie SOLIDTIME_IMAGE_TAG in .env auf die neue Version. Da AUTO_DB_MIGRATE im app-Container gesetzt ist, reichen dann zwei Befehle:
cd /opt/solidtime
docker compose pull
docker compose up -d
# Optional: alte Images aufräumen
docker image prune -fOhne AUTO_DB_MIGRATE migrieren Sie nach jedem Update manuell:
docker compose exec scheduler php artisan migrate --forceUpdate von 0.21.0 auf 0.22.0: Die Version bringt Ziele (Goals) für Stundenvorgaben pro Tag, Woche oder Monat. Für den Betrieb wichtig: Die Variable für den SSL-Modus der Datenbank heißt jetzt DB_SSL_MODE statt DB_SSLMODE; der alte Name wurde laut Release Notes ohnehin nicht korrekt ausgewertet. Die laravel.env dieser Anleitung setzt keinen der beiden Werte, dann gilt der Standard prefer. Wer DB_SSLMODE=require eingetragen hat, benennt die Zeile um; mit dem mitgelieferten PostgreSQL-Container ohne TLS bricht require die Verbindung ab, das offizielle Beispiel nutzt deshalb prefer. Im Test spielte der app-Container beim Start drei neue Migrationen ein (62 auf 65), alle fünf Container waren wieder healthy, ein vorher angelegter Kunde und der Admin blieben erhalten, und das bestehende Passwort war weiter gültig. Eine Neuinstallation von 0.22.0 mit dieser Anleitung lief ebenfalls durch: Anmeldung über die Domain aus APP_URL mit 200, falsches Passwort mit 422 abgelehnt.
docker compose ps zeigt für app, scheduler und queue das neue Tag und den Status healthy; docker compose logs app | grep -i migrat enthält keine Fehler.
Troubleshooting / Typische Fehler
- „500 Internal Server Error“ oder „No application encryption key has been specified“
APP_KEYinlaravel.envist leer. Führen Siedocker compose run --rm scheduler php artisan self-host:generate-keysaus und tragen Sie 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. Setzen Sie
AUTO_DB_MIGRATE: "true"im app-Service oder führen Sie 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 schlägt fehl
Gotenberg läuft nicht (
docker compose ps gotenberg) oderGOTENBERG_URLfehlt inlaravel.env.- E-Mails werden nicht versendet
MAIL_*-Variablen fehlen; ohne SMTP funktioniert der Passwort-Reset nicht.- Mixed Content hinter dem Reverse Proxy
APP_URLohnehttps://oderAPP_FORCE_HTTPSfehlt; korrigieren und Stack neu starten.- Verwechslung der beiden Konfigurationsdateien
Aufteilung siehe Schritt 2; Variablen in der falschen Datei wirken nicht.
- Nach Update fehlen Features oder Fehlermeldungen in Logs
DB-Migration nach Image-Update vergessen.
AUTO_DB_MIGRATEim app-Service setzen oder nach jedem Update manuell migrieren.
Häufige Fragen
Kann ich Solidtime ohne Reverse Proxy betreiben?
Für Tests ja: Entfernen Sie dazu 127.0.0.1: aus der Portzuordnung, setzen Sie APP_URL auf http://<IP>:8000 und APP_FORCE_HTTPS=false, und nutzen Sie das nur im internen Netz. Für den Produktivbetrieb gehört ein Reverse Proxy mit HTTPS davor.
Wie importiere ich Daten aus Toggl oder Clockify?
Solidtime bietet einen Import direkt in der Web-UI: Einstellungen → Import. Unterstützt werden Toggl, Clockify und generische Zeiteintrags-CSV-Dateien. Den Import verarbeitet der queue-Container, er muss also laufen.
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 ergänzt Support, SLA und Enterprise-Funktionen. Für die meisten Agenturen und Freelancer reicht die Community-Variante.
Wie sichere ich die Solidtime-Daten?
Das benannte Docker-Volume database-storage enthält alle PostgreSQL-Daten. Die Sicherung per Dump beschreibt die Anleitung MySQL & PostgreSQL Backup automatisieren. Sichern Sie zusätzlich /opt/solidtime/app-storage, dort liegen Uploads.
Kann ich die Image-Version fest einpinnen?
Ja, diese Anleitung tut das bereits: SOLIDTIME_IMAGE_TAG=0.22.0 in .env. Die Docker-Hub-Tags haben kein „v“ vorangestellt (0.22.0, nicht v0.22.0). Aktuelle Releases finden Sie auf der Releases-Seite des Haupt-Repositories.
Fazit
Solidtime deckt Projekte, Kunden, abrechenbare Stunden und Import aus bestehenden Tools ohne Cloud-Abhängigkeit ab. Stolpersteine sind die zwei getrennten Dateien .env und laravel.env sowie die Key-Generierung. Mit festem Image-Tag, AUTO_DB_MIGRATE im app-Container und regelmäßigen Datenbank-Backups bleibt der Betrieb wartungsarm.
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)


