Self-hosted time tracking via YAML. Timer, dark mode, heatmap, backups, CSV import/export, ntfy push
320
Self-hosted time tracking, configured entirely through a single YAML file.
Hustlify is a small, neutral time-management web app built to run as one Docker
container on a home server or NAS. Track work with a live timer or by hand,
organize it into categories, watch your weekly target, and export a clean PDF
work report — all stored in a local SQLite database. Nothing is hardcoded to any
person or company: everything you'd want to change lives in config.yaml.


See CHANGELOG.md for the full history.
Create a folder on your NAS and add a config.yaml (start from
config.example.yaml):
app:
title: "My Time Tracker"
timezone: "Europe/Zurich"
work:
weekly_target_hours: 40
hourly_rate: 0
currency: "CHF"
Add a docker-compose.yml (see docker-compose.example.yml):
services:
hustlify:
image: julianhintermann/hustlify:latest
container_name: hustlify
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./config.yaml:/config/config.yaml:ro
- ./data:/data
Start it:
docker compose up -d
Open http://<your-nas-ip>:3000.
That's it — the SQLite database and session secret are created automatically in
the ./data folder.
docker run -d --name hustlify -p 3000:3000 \
-v "$PWD/config.yaml:/config/config.yaml:ro" \
-v "$PWD/data:/data" \
julianhintermann/hustlify:latest
Every key is optional; omitted keys use the defaults shown here.
| Key | Default | Description |
|---|---|---|
app.title | Hustlify | Name shown in the header and browser tab. |
app.port | 3000 | Port the server listens on inside the container. |
app.timezone | Europe/Zurich | IANA timezone for day/week/month grouping and reports. |
app.first_day_of_week | monday | monday or sunday. |
auth.password | "" | Empty = no login. Any value requires it before use. |
work.weekly_target_hours | 40 | Weekly target. 0 disables overtime tracking. |
work.tracking_start | "" | ISO date (YYYY-MM-DD); overtime is counted from here. |
work.hourly_rate | 0 | 0 hides earnings everywhere; any value shows estimates. |
work.currency | CHF | Currency label shown next to earnings. |
work.long_timer_warning_hours | 10 | Warn on the timer card past this many hours running. 0 disables it. |
work.idle_detection_minutes | 5 | Prompt to trim idle time after the tab was hidden/asleep this long while a timer runs. 0 disables it. |
report.person_name | "" | Printed on the PDF work report. |
report.company_name | "" | Optional company/employer on the report. |
report.footer_note | "" | Optional note at the bottom of the report. |
notifications.ntfy_url | "" | e.g. http://192.168.1.125:3061/hustlify. Empty disables all push notifications. |
notifications.timer_reminder | true | Push once when a running timer passes long_timer_warning_hours. |
notifications.daily_summary_at | "" | "HH:MM" local time for a daily push with today's tracked hours. Empty = off. |
notifications.weekly_summary_at | "" | "HH:MM" local time on first_day_of_week for a weekly summary push. Empty = off. |
Note: if you change
app.port, map that same port in your compose file (e.g."8080:8080"withapp.port: 8080).
| Path | Purpose |
|---|---|
/config/config.yaml | Your configuration file (mount read-only). |
/data | Persistent SQLite database and the generated session secret. |
Download a complete, consistent copy of the database at any time from the
Reports view ("Download database backup"), or directly via GET /api/backup.db.
To restore, use "Restore from backup" in the same view — the uploaded file is
validated (it must genuinely be a Hustlify database) and then swapped in while
the app keeps running, no container restart needed.
Point notifications.ntfy_url at any ntfy topic — your own
self-hosted instance or the public one — to enable push notifications:
notifications:
ntfy_url: "https://ntfy.sh/your-private-topic-name"
daily_summary_at: "18:00"
Install the ntfy app and subscribe to the same topic to receive the pushes.
"Import CSV" in the Reports view accepts a file in the same shape Hustlify
exports (Date,Start,End,Duration,Hours,Category,Note). Any category name
that doesn't already exist is created automatically. Invalid rows are skipped
with a reason rather than failing the whole import.
Requires Node.js 22.5+ (uses the built-in node:sqlite, so there is no native
build step).
npm install # backend deps
npm --prefix web install
npm test # backend unit + integration tests
# Run the two dev servers (API on :3000, Vite on :5173 with a proxy)
npm run dev:server
npm run dev:web
# Or build the frontend and run the whole app from the server on :3000
npm run build
npm start
Configuration during development is read from ./config.yaml (set
CONFIG_PATH to point elsewhere) and data is written to ./data (set
DATA_DIR).
node:sqlite, pdfkit, js-yamllinux/amd64, linux/arm64)MIT — see LICENSE.
Content type
Image
Digest
sha256:c8911760c…
Size
86.8 MB
Last updated
4 months ago
docker pull julianhintermann/hustlify