Self-Hosted Time Tracking with TimeTracker: Ditch Toggl, Own Your Data, and Save $1,000+ a Year
“Your invoice for tracking time just arrived—and it’s bigger than your hourly rate.”
If that sentence stings, this post is for you.
1. The Pain You Know Too Well
Picture 1 A.M. You’ve shipped the weekly report, but the SaaS time-tracker greets you with:
“Export limit reached—upgrade to Pro.”
Eight seats × 1,150. Data still lives on their S3.
Oh, idle detection? Locked behind the “Enterprise” tier.
Sound familiar?
TimeTracker—an MIT-licensed, Docker-first alternative—lets you swap that rent for a single VPS and five minutes of CLI. Below is the only guide you need to go from zero to production, including the security and tuning notes I wish I’d had.
2. Why TimeTracker Is “Professional-Grade”
Stack: Flask, SQLAlchemy, Celery, Socket.IO, Postgres/SQLite, Alpine.js
-
Persistent timers live server-side—close your laptop, open your phone, timer keeps ticking. -
WebSocket sync < 150 ms worldwide (Cloudflare proxy tested). -
Built-in invoicing with tax, discounts, multi-currency. -
Unlimited users—no seat math, ever.
3. Three Deployment Paths (Pick One)
3.1 5-Minute Taste (SQLite, local only)
git clone https://github.com/drytrix/TimeTracker.git && cd TimeTracker
docker compose -f docker-compose.local-test.yml up --build
Visit http://localhost:8080
; the first username you enter becomes admin.
⚠️ Data dies with the container—perfect for a spin-up demo.
3.2 Production Route (Postgres + SSL + Backups)
-
Create volumes
mkdir -p /srv/timetracker/{db,uploads,backup}
chmod 0755 /srv/timetracker
-
.env
(never commit to Git)
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
SQLALCHEMY_DATABASE_URI=postgresql://tt_user:TTstrongPWD@db:5432/timetracker
TZ=America/New_York
CURRENCY=USD
SESSION_COOKIE_SECURE=true
-
Launch
docker compose -f docker-compose.remote.yml up -d
docker compose exec web flask db upgrade # first run only
-
Nginx snippet (TLS via Let’s Encrypt)
server {
server_name tt.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; # WebSocket
}
}
-
Daily backup cron
0 2 * * * docker exec timetracker-db pg_dump -U tt_user timetracker | gzip > /srv/timetracker/backup/tt_$(date +\%F).sql.gz
3.3 Raspberry Pi 4 (ARM64)
Same compose file; image is multi-arch. Use an A2-rated SD or, better, USB-SSD to prolong card life.
4. Deep Dive: Features That Matter
4.1 Persistent Timer (a.k.a. “close the lid, keep the clock”)
-
Clicking Start inserts a row status='R'
. -
Server increments every second; WebSocket broadcasts {entry_id, cumulated_seconds}
. -
Re-open any device → instant catch-up; no local storage conflict resolution.
4.2 Idle Detection
Client samples mouse/keyboard; after 30 s of silence it pings /idle
.
Celery task pauses the entry and logs idle_seconds
so you can strip away coffee breaks from client invoices.
4.3 Invoicing in One Click
# app/invoices/services.py (abridged)
hours = db.session.query(func.sum(TimeEntry.duration))\
.filter_by(client_id=client_id)\
.filter(TimeEntry.started_at.between(from_date, to_date))\
.scalar() or 0
pdf = HTML(string=render_template('invoice_pdf.html', total=hours*client.rate)).write_pdf()
Add expenses, choose tax, PDF is ready in < 2 s.
5. Performance & Observability
Endpoint | 100 RPS | p99 latency |
---|---|---|
Start timer | 42 ms | 110 ms |
Stop timer | 38 ms | 95 ms |
Year report | 2.1 s → 180 ms (after index) |
Monitoring: /metrics
endpoint compatible with Prometheus/grafana dashboard ID 18630.
6. Security Hardening Checklist
✅ SECRET_KEY
≥ 32 random bytes
✅ CSRF double-submit cookie; token rotated per session
✅ Admin route rate-limited (fail2ban)
✅ Database row-level encryption for client VAT numbers (pgcrypto
)
✅ Container runs as non-root, read-only root-fs, CAP_DROP ALL
7. FAQ (People Also Ask)
Q1: Does it import Toggl/Clockify CSV?
A: Yes. Upload → map columns → projects & tags auto-created.
Q2: Will updates break my database?
A: Migrations use Alembic; always flask db upgrade
after pulling a new image. Rollback with flask db downgrade
.
Q3: Can I run it behind Cloudflare Tunnel?
A: Absolutely. Just set SESSION_COOKIE_SECURE=true
and forward X-Forwarded-Proto=https
.
Q4: Is mobile app support planned?
A: PWA manifest already merged; “Add to Home Screen” works on iOS/Android today. Native wrappers coming Q4.
8. Takeaway
Every hour you track on someone else’s cloud is a line-item you’ll pay for forever.
TimeTracker turns that OPEX into a one-time setup and gives you:
-
Zero seat limits -
Full data ownership -
Invoice-to-bank in five clicks
Clone the repo, run the compose file, and start billing hours the self-hosted way.
Star the project if it saves you a buck—or a thousand.
Happy timing, and may your docker ps
always show “Up 3 days”.