Wakapi mit Docker Compose: WakaTime-Statistik selbst hosten
Wakapi ist ein schlanker, WakaTime-kompatibler Server für Coding-Statistiken. Die Anleitung zeigt die Installation mit Docker Compose und PostgreSQL 17, die Anbindung der Editor-Plugins, einen Heartbeat-Test per curl, Sicherung mit pg_dump, eine echte Wiederherstellung und das Update von 2.18.1 auf 2.18.2.
WerbelinksMit * 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

Wer wissen möchte, wie viel Zeit in welches Projekt, welche Sprache und welchen Editor fließt, landet schnell bei WakaTime, dessen Auswertung in der Cloud des Anbieters läuft. Wakapi ist ein schlanker, in Go geschriebener Server, der die WakaTime-API nachbildet. Sie behalten also die gewohnten Plugins und schicken die Heartbeats an den eigenen Server. Diese Anleitung zeigt den Betrieb mit Docker Compose und PostgreSQL, die Anbindung der Clients, eine Sicherung samt Wiederherstellung und das Update auf eine neue Version.
Voraussetzungen
Wakapi selbst ist sehr genügsam, den größten Teil des Speichers belegt PostgreSQL. Im Test lagen beide Container zusammen bei rund 125 MiB RAM.
- Linux-Server oder VM mit 1 bis 2 CPU-Kernen, 1 GB RAM (2 GB mit Reserve) und 5 GB freiem Speicher, x86_64 oder ARM64
- Docker Engine mit Compose-Plugin v2
- Eine Domain mit TLS-Reverse-Proxy, falls Clients außerhalb des LAN senden sollen
- Auf den Entwickler-Rechnern ein WakaTime-Plugin für den jeweiligen Editor
opensslzum Erzeugen von Zufallswerten, optionaljqfür API-Tests
| Eckdaten | Wert |
|---|---|
| Image | ghcr.io/muety/wakapi:2.18.2 (auch 2.18, 2, latest) |
| Datenbank | postgres:17, Volume wakapi-db-data unter /var/lib/postgresql/data |
| Port | 3000 (HTTP, Weboberfläche und API) |
| Wichtige Variablen | WAKAPI_DB_TYPE, WAKAPI_PASSWORD_SALT, WAKAPI_COOKIE_KEY, WAKAPI_PUBLIC_URL, WAKAPI_ALLOW_SIGNUP |
| Lizenz | MIT |
Schritt 1: Compose-Datei anlegen
Grundlage ist die compose.yml aus dem Projekt. Sie baut das Image allerdings lokal mit build: ., was für den Betrieb unpraktisch ist. Die folgende Fassung nutzt stattdessen das fertige Image aus der GitHub Container Registry, ergänzt einen Healthcheck für PostgreSQL und lässt den SMTP-Teil weg. Speichern Sie sie als /opt/wakapi/compose.yaml:
services:
wakapi:
image: ghcr.io/muety/wakapi:${WAKAPI_TAG}
init: true
restart: unless-stopped
ports:
- "3000:3000"
environment:
WAKAPI_DB_TYPE: "postgres"
WAKAPI_DB_NAME: "wakapi"
WAKAPI_DB_USER: "wakapi"
WAKAPI_DB_HOST: "db"
WAKAPI_DB_PORT: "5432"
WAKAPI_DB_PASSWORD_FILE: "/run/secrets/db_password"
WAKAPI_PASSWORD_SALT_FILE: "/run/secrets/password_salt"
WAKAPI_PUBLIC_URL: "${WAKAPI_PUBLIC_URL}"
WAKAPI_ALLOW_SIGNUP: "${WAKAPI_ALLOW_SIGNUP}"
WAKAPI_LEADERBOARD_ENABLED: "false"
WAKAPI_COOKIE_KEY: "${WAKAPI_COOKIE_KEY}"
WAKAPI_INSECURE_COOKIES: "${WAKAPI_INSECURE_COOKIES}"
TZ: "Europe/Berlin"
secrets:
- source: password_salt
target: password_salt
uid: "65532"
gid: "65532"
mode: 0400
- source: db_password
target: db_password
uid: "65532"
gid: "65532"
mode: 0400
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: "wakapi"
POSTGRES_DB: "wakapi"
POSTGRES_PASSWORD_FILE: "/run/secrets/db_password"
volumes:
- wakapi-db-data:/var/lib/postgresql/data
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U wakapi -d wakapi"]
interval: 10s
timeout: 5s
retries: 10
secrets:
password_salt:
environment: WAKAPI_PASSWORD_SALT
db_password:
environment: WAKAPI_DB_PASSWORD
volumes:
wakapi-db-data: {}Die Secrets gehören der UID 65532, weil das Distroless-Image als Benutzer nonroot läuft. depends_on mit service_healthy verhindert, dass Wakapi vor der Datenbank startet. Die öffentliche Bestenliste ist ab Werk eingeschaltet und für eine interne Instanz meist unerwünscht, daher WAKAPI_LEADERBOARD_ENABLED: "false". PostgreSQL 17 nutzt noch den klassischen Pfad /var/lib/postgresql/data; wer auf Version 18 wechselt, muss das Volume auf /var/lib/postgresql umhängen.
Verifizieren: docker compose config --quiet im Projektordner gibt nichts aus und endet mit Exit-Code 0. Fehlt später die .env, warnt Compose an dieser Stelle über leere Variablen.
Schritt 2: Geheimnisse erzeugen und starten
Die .env enthält den Image-Tag, die öffentliche Adresse und drei Zufallswerte. Das Passwort-Salt wird in jeden Passwort-Hash eingerechnet: Geht es verloren, kann sich niemand mehr mit dem bisherigen Passwort anmelden. Der Cookie-Key signiert die Sitzungen.
mkdir -p /opt/wakapi && cd /opt/wakapi
cat > .env <<EOF
WAKAPI_TAG=2.18.2
WAKAPI_PUBLIC_URL=https://wakapi.example.de
WAKAPI_ALLOW_SIGNUP=true
WAKAPI_INSECURE_COOKIES=true
WAKAPI_PASSWORD_SALT=$(openssl rand -hex 24)
WAKAPI_DB_PASSWORD=$(openssl rand -hex 24)
WAKAPI_COOKIE_KEY=$(openssl rand -base64 32)
EOF
chmod 600 .envDen Cookie-Key sollten Sie nicht weglassen, auch wenn die Projektdoku ihn als optional führt. Ohne ihn erzeugt Wakapi bei jedem Start einen neuen Schlüssel. Im Test lieferte eine gültige Sitzung vor dem Neustart HTTP 200 auf /summary, danach nur noch eine 302-Weiterleitung zum Login. Mit gesetztem WAKAPI_COOKIE_KEY blieb die Sitzung über den Neustart hinweg gültig. Starten Sie den Stack anschließend:
docker compose up -d
docker compose ps
docker compose logs --no-log-prefix wakapi | grep "Wakapi version"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/healthBeim ersten Start legt Wakapi das Schema an und führt eine Reihe von Migrationen aus. Der Test startete zunächst mit 2.18.1, um in Schritt 8 das Update zu prüfen. Der Container war nach rund 20 Sekunden healthy, die Ausgabe sah so aus:
news-wakapi-db-1 postgres:17 Up 39 seconds (healthy)
news-wakapi-wakapi-1 ghcr.io/muety/wakapi:2.18.1 Up 18 seconds (healthy)
INFO Wakapi version="v2.18.1"
200Verifizieren: Beide Container stehen auf healthy, das Log nennt die erwartete Version und /api/health antwortet mit 200.
Schritt 3: Erstes Konto anlegen und Registrierung schließen
Öffnen Sie http://SERVER:3000/signup oder Ihre Domain und legen Sie ein Konto an. Die E-Mail-Adresse ist optional, wird aber für Passwort-Resets und Wochenberichte gebraucht. Das erste Konto einer leeren Datenbank erhält automatisch Administratorrechte; in der Tabelle users stand im Test bei diesem Konto is_admin = t, bei den folgenden f.

Legen Sie anschließend alle Konten für Ihr Team an und setzen Sie dann in der .env WAKAPI_ALLOW_SIGNUP=false. Ein docker compose up -d erzeugt den Container neu. Für spätere Konten sieht Wakapi Einladungscodes vor (Option WAKAPI_INVITE_CODES, ab Werk aktiv).
Verifizieren: Ein erneuter Registrierungsversuch per curl -d "username=gast&password=Testtest123&password_repeat=Testtest123" http://localhost:3000/signup liefert im Test HTTP 403 mit dem Text „Registration is disabled on this server“.
Schritt 4: WakaTime-Clients anbinden
Unter Settings im Reiter API Keys finden Sie den Hauptschlüssel Ihres Kontos. Dort lassen sich auch zusätzliche Schlüssel anlegen, wahlweise nur lesend, etwa für ein Dashboard, das Statistiken abfragt, aber keine Heartbeats senden darf.

Auf jedem Entwickler-Rechner tragen Sie Server und Schlüssel in die Datei ~/.wakatime.cfg ein. Wichtig ist das Suffix /api in der URL:
[settings]
api_url = https://wakapi.example.de/api
api_key = IHR-API-KEYOb der Server Heartbeats annimmt, prüfen Sie unabhängig vom Editor direkt per curl. Wakapi erwartet den API-Key Base64-kodiert im Header Authorization: Basic, genau wie die WakaTime-Clients ihn senden:
KEY=IHR-API-KEY
AUTH=$(printf %s "$KEY" | base64 -w0)
curl -s -w "\n%{http_code}\n" \
-A "wakatime/v1.131.0 (linux-6.12-x86_64) go1.24 vscode/1.105.0 vscode-wakatime/25.3.0" \
-H "Authorization: Basic $AUTH" -H "Content-Type: application/json" \
-d "{\"entity\":\"/home/dev/kundenportal/app.ts\",\"type\":\"file\",\"category\":\"coding\",
\"project\":\"kundenportal\",\"language\":\"TypeScript\",\"time\":$(date +%s)}" \
http://localhost:3000/api/heartbeatDie Antwort lautet {"responses":[[{"data":null,"error":null},201]]} mit Status 201. Für viele Heartbeats auf einmal gibt es /api/users/current/heartbeats.bulk, das ein JSON-Array annimmt. Ein falscher Schlüssel führt zu HTTP 401.
Verifizieren: Der curl-Aufruf liefert 201, und in der Weboberfläche taucht das Projekt kundenportal unter Projects auf.
Schritt 5: Statistik prüfen
Wakapi fasst die Heartbeats zu Zeitspannen zusammen. Im Test erzeugten elf Heartbeats im Abstand von je zwei Minuten genau 20 Minuten Projektzeit, die Lücken dazwischen zählen also als durchgehend gearbeitet. Die Daten lesen Sie über die WakaTime-kompatible API aus:
curl -s -H "Authorization: Basic $AUTH" \
"http://localhost:3000/api/compat/wakatime/v1/users/current/stats/last_7_days" \
| jq -c '.data|{total_seconds,human_readable_total,projects:[.projects[]|{name,text}]}'{"total_seconds":3600,"human_readable_total":"1 hrs 0 mins",
"projects":[{"name":"restore-marker","text":"0 hrs 40 mins"},
{"name":"sedv-demo","text":"0 hrs 20 mins"}]}
Der Editor wird aus dem User-Agent abgeleitet, nicht aus einem JSON-Feld. Heartbeats, die im Test ohne WakaTime-User-Agent per curl kamen, landeten im Dashboard unter dem Editor „Unknown“; mit dem User-Agent eines VS-Code-Plugins erschien korrekt „Vscode“.
Verifizieren: Die API meldet eine Gesamtzeit größer null, und das Dashboard zeigt unter Past 7 Days dieselben Projekte und Sprachen.
Schritt 6: Datenbank sichern
Alle Nutzdaten liegen in PostgreSQL. Das Wakapi-Image ist distroless und enthält keine Shell, Sicherungen laufen deshalb über den Datenbank-Container. Das Format -Fc ist komprimiert und lässt sich gezielt mit pg_restore einspielen. Sichern Sie zusätzlich die .env, denn ohne das alte Passwort-Salt sind die gespeicherten Passwort-Hashes wertlos.
cd /opt/wakapi && mkdir -p backup
docker compose exec -T db pg_dump -U wakapi -d wakapi -Fc > backup/wakapi-$(date +%F).dump
cp .env backup/env-$(date +%F)
ls -la backupIm Test war der Dump mit zwei Konten und 47 Heartbeats rund 40 KB groß. Legen Sie den Befehl in einen nächtlichen Cronjob und kopieren Sie den Ordner backup auf ein anderes System, etwa mit restic.
Verifizieren: ls -la backup zeigt Dump und .env-Kopie; der Dump muss deutlich größer als null Byte sein. Ob er sich einspielen lässt, beweist erst Schritt 7.
Schritt 7: Wiederherstellung durchspielen
Eine Sicherung ist erst etwas wert, wenn die Rücksicherung einmal funktioniert hat. Im Test diente ein Projekt namens restore-marker mit 23 Heartbeats als Markierung. Stoppen Sie zuerst Wakapi, damit keine offene Verbindung das Löschen der Datenbank blockiert, und spielen Sie den Dump dann in eine frisch angelegte, leere Datenbank:
docker compose stop wakapi
docker compose exec -T db dropdb -U wakapi --maintenance-db=postgres wakapi
docker compose exec -T db createdb -U wakapi --maintenance-db=postgres -O wakapi wakapi
docker compose exec -T db pg_restore -U wakapi -d wakapi --exit-on-error --no-owner \
< backup/wakapi-2026-10-21.dump; echo "restore exit=$?"
docker compose start wakapivorher: users=2 hb=47 marker=23
leer: tables=0
restore exit=0
nachher: users=2 hb=47 marker=23
health=200 nach 2x2sZwei Stolpersteine traten dabei auf. dropdb und createdb kennen keine Option -d und melden invalid option -- 'd'; die Wartungsdatenbank wird mit --maintenance-db=postgres angegeben. Und wer den Dump in die bestehende, noch gefüllte Datenbank einspielt, bekommt ERROR: relation "aliases" already exists, weil Wakapi das Schema bereits angelegt hat.
Verifizieren: Nach dem Start meldet /api/health 200, die Stats-API zeigt wieder das Projekt restore-marker, und der API-Key aus der Sicherung wird weiter akzeptiert.
Schritt 8: Auf eine neue Version aktualisieren
Der Test startete bewusst mit Version 2.18.1 und wurde dann auf 2.18.2 angehoben. Ändern Sie den Tag in der .env, ziehen Sie das Image und erstellen Sie den Container neu. Sichern Sie vorher die Datenbank wie in Schritt 6.
sed -i 's/^WAKAPI_TAG=.*/WAKAPI_TAG=2.18.2/' .env
docker compose pull wakapi
docker compose up -d
docker compose logs --no-log-prefix wakapi | grep -E "idx_user_project_time|Wakapi version"INFO Wakapi version="v2.18.2"
INFO index 'idx_user_project_time' needs to be created, this may take a whileVersion 2.18.2 legt laut Release-Notes einen neuen Index auf der Tabelle heartbeats an. Auf der kleinen Testdatenbank dauerte das etwa eine Sekunde; bei Millionen Heartbeats warnt das Projekt vor Stunden. Wer nicht so lange warten will, legt den Index vor dem Update selbst an: create index idx_user_project_time on heartbeats (user_id, project, time);. Die Release-Notes von 2.18.1 weisen außerdem auf fehlschlagende Migrationen unter MariaDB hin, PostgreSQL ist davon nicht betroffen. Nach dem Update waren alle Heartbeats und Statistiken unverändert vorhanden, und die vor dem Update erzeugte Sitzung blieb dank festem Cookie-Key gültig.
Verifizieren: docker compose exec wakapi /app/wakapi -version gibt 2.18.2 aus, die Fußzeile der Weboberfläche zeigt v2.18.2 @ postgres.
Troubleshooting
| Symptom | Ursache | Lösung |
|---|---|---|
| Nach jedem Neustart abgemeldet | Kein WAKAPI_COOKIE_KEY, Schlüssel wird zufällig neu erzeugt | Festen Base64-Wert in die .env |
| Heartbeats ergeben HTTP 401 | API-Key falsch oder nicht Base64-kodiert | Schlüssel aus API Keys kopieren, Header prüfen |
| Editor erscheint als „Unknown“ | Kein WakaTime-User-Agent | Echte Plugins senden ihn; bei Skripten mit -A setzen |
relation "aliases" already exists | Restore in eine gefüllte Datenbank | Datenbank löschen, leer anlegen, dann pg_restore |
invalid option -- 'd' bei dropdb | Falscher Parameter | --maintenance-db=postgres |
| Erster Start nach Update dauert sehr lange | Indexaufbau auf großer Heartbeat-Tabelle | Index vorab manuell anlegen, Healthcheck-Startzeit erhöhen |
Hinter einem Reverse Proxy mit HTTPS empfiehlt die Projektdoku WAKAPI_INSECURE_COOKIES=false, damit das Sitzungscookie nur verschlüsselt übertragen wird. Lassen Sie den Wert auf true, solange Sie die Instanz noch per HTTP über Port 3000 aufrufen, sonst verwirft der Browser das Cookie.
Häufige Fragen
Kann ich WakaTime und Wakapi parallel nutzen?
Ja. Die WakaTime-CLI kann im Abschnitt [api_urls] der .wakatime.cfg weitere Ziele mit eigenem Schlüssel bekommen. Alternativ leitet Wakapi Heartbeats über Settings und Integrations an WakaTime weiter.
Reicht SQLite statt PostgreSQL?
Für eine einzelne Person ja, SQLite ist der Standard des Images und liegt unter /data/wakapi.db. Für Teams und große Datenmengen ist PostgreSQL robuster und lässt sich im laufenden Betrieb konsistent sichern.
Lassen sich alte WakaTime-Daten übernehmen?
Wakapi bietet in den Einstellungen einen Import über die WakaTime-API an. Dafür ist der WakaTime-API-Key nötig; der Import ist standardmäßig erlaubt und lässt sich mit WAKAPI_IMPORT_ENABLED=false abschalten.
Fazit
Wakapi ist eine leichte Alternative zur WakaTime-Cloud: Die bekannten Editor-Plugins bleiben, die Daten liegen auf dem eigenen Server. Die Einrichtung mit PostgreSQL ist in einer Viertelstunde erledigt, Sicherung und Wiederherstellung funktionieren mit Bordmitteln von PostgreSQL. Wichtig sind ein fester Cookie-Key, die gesicherte .env mit dem Passwort-Salt und die abgeschaltete Registrierung. Vor Updates lohnt der Blick in die Release-Notes.
Weiterführende Anleitungen und Quellen
- PostgreSQL mit pg_dump und pg_restore sichern und migrieren
- Caddy als Reverse Proxy mit automatischem HTTPS
- Bugsink mit Docker Compose: Error-Tracking selbst hosten
- Wakapi auf GitHub
- Offizielle compose.yml (Version 2.18.2)
- config.default.yml mit allen Optionen
- Release-Notes Wakapi 2.18.2
- Container-Image in der GitHub Container Registry
- PostgreSQL-Dokumentation zu pg_restore


