As subscription fatigue grows and data-sovereignty requirements tighten, more teams are looking for self-hosted productivity apps that don’t lock their data into third-party clouds. TimeTracker 1.3.0—a Flask-based web application with PostgreSQL—lands as a credible, lightweight alternative to SaaS tools like Toggl or Harvest, pairing project/time tracking and invoicing with exportable analytics and a responsive UI that works well on mobile.

What it does

  • Time tracking: Start/stop timers, manual entries, idle-detection (configurable), single-active-timer mode.
  • Project & client management: Client defaults (rates), task categorization, status/archival.
  • Billing & exports: Billable vs. non-billable, CSV export with ISO-8601 timestamps and configurable delimiters.
  • Analytics: Per project/user reports, daily/weekly/monthly summaries, trend charts.
  • API & real-time updates: REST API for integrations; WebSockets for live timers.
  • Multi-user & roles: Team-ready with admin/user roles.
  • Mobile-ready: Responsive UI and PWA behaviour for phones and tablets.
TimeTracker 1.3.0: a lightweight, self-hosted alternative for time tracking, invoicing and analytics | timetracker Dashboard
TimeTracker 1.3.0: a lightweight, self-hosted alternative for time tracking, invoicing and analytics

What’s new in 1.3.0

  • Sharper reporting & exports with flexible date ranges and cleaner CSVs.
  • UI refinements for small screens and faster navigation.
  • Backend performance improvements in common queries.
  • Stability fixes across timers and idle detection.

Why teams pick it

  • Own your data. Run it on-prem or in your VPC; align with privacy/sovereignty policies.
  • No per-seat fees. Open source (GPLv3) with zero licensing costs.
  • Hackable. Clean Flask structure, SQLAlchemy models, Flask-Migrate, and a documented REST API.

Repo: https://github.com/DRYTRIX/TimeTracker


Production deployment on a Linux server (Docker + Docker Compose)

The steps below assume a fresh Ubuntu/Debian host with a non-root user in the docker group and a DNS record pointing to your server (optional but recommended for HTTPS).

0) Install prerequisites

# Docker Engine + Compose plugin (Debian/Ubuntu quick path)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# log out/in to apply group membership, then:
docker --version && docker compose version
Code language: PHP (php)

1) Create a project directory and .env

mkdir -p ~/timetracker && cd ~/timetracker
cat > .env << 'EOF'
# General
TZ=Europe/Madrid
CURRENCY=EUR
ROUNDING_MINUTES=1
SINGLE_ACTIVE_TIMER=true
ALLOW_SELF_REGISTER=true
IDLE_TIMEOUT_MINUTES=30
ADMIN_USERNAMES=admin

# Security (CHANGE in production)
SECRET_KEY=$(openssl rand -hex 32)
SESSION_COOKIE_SECURE=true
REMEMBER_COOKIE_SECURE=true

# Database
POSTGRES_DB=timetracker
POSTGRES_USER=timetracker
POSTGRES_PASSWORD=$(openssl rand -hex 24)

# Optional for reverse proxy
VIRTUAL_HOST=timetracker.example.com
EOF
Code language: PHP (php)

Replace timetracker.example.com with your real domain. If you won’t use HTTPS yet, you can omit VIRTUAL_HOST and set SESSION_COOKIE_SECURE=false.

2) Create a production-ready docker-compose.yml

This uses the published image and a separate PostgreSQL container. It also enables automatic DB migrations on startup via the app’s entrypoint logic.

services:
  db:
    image: postgres:16-alpine
    container_name: timetracker-db
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      TZ: ${TZ}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10

  app:
    image: ghcr.io/drytrix/timetracker:latest
    container_name: timetracker-app
    depends_on:
      db:
        condition: service_healthy
    environment:
      TZ: ${TZ}
      CURRENCY: ${CURRENCY}
      ROUNDING_MINUTES: ${ROUNDING_MINUTES}
      SINGLE_ACTIVE_TIMER: ${SINGLE_ACTIVE_TIMER}
      ALLOW_SELF_REGISTER: ${ALLOW_SELF_REGISTER}
      IDLE_TIMEOUT_MINUTES: ${IDLE_TIMEOUT_MINUTES}
      ADMIN_USERNAMES: ${ADMIN_USERNAMES}
      SECRET_KEY: ${SECRET_KEY}
      SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE}
      REMEMBER_COOKIE_SECURE: ${REMEMBER_COOKIE_SECURE}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_HOST: db
      POSTGRES_PORT: 5432
    ports:
      - "8080:8080"
    volumes:
      - app_logs:/app/logs
    restart: unless-stopped

volumes:
  db_data:
  app_logs:
Code language: JavaScript (javascript)

Start the stack:

docker compose up -d
docker compose ps

Access the app at: http://SERVER_IP:8080

First run can take a minute while migrations apply.

3) (Recommended) Enable HTTPS with Caddy as a reverse proxy

If you have a domain (timetracker.example.com) pointing to your server’s public IP, Caddy will fetch/renew Let’s Encrypt certificates automatically.

Create docker-compose.proxy.yml:

services:
  proxy:
    image: caddy:2.8-alpine
    container_name: timetracker-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app
    restart: unless-stopped

volumes:
  caddy_data:
  caddy_config:
Code language: PHP (php)

Create Caddyfile in the same folder:

timetracker.example.com {
    encode zstd gzip
    reverse_proxy app:8080
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
    }
    @assets {
        path /static/* /favicon.ico
    }
    handle @assets {
        header Cache-Control "public, max-age=31536000, immutable"
        reverse_proxy app:8080
    }
}
Code language: PHP (php)

Bring up the proxy alongside the app:

docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
Code language: CSS (css)

Now visit https://timetracker.example.com.
Since HTTPS is on, keeping SESSION_COOKIE_SECURE=true is correct.

4) Create the first admin user

If self-registration is enabled, you can sign up and then promote a user via the admin panel. If you prefer CLI:

# Exec into the app container (adjust username/email as needed)
docker exec -it timetracker-app bash -lc '
python - <<PY
from app import create_app, db
from app.models import User
app = create_app()
with app.app_context():
    u = User(username="admin", email="[email protected]")
    u.set_password("CHANGE_THIS_PASSWORD")
    u.is_admin = True
    db.session.add(u)
    db.session.commit()
    print("Admin user created.")
PY'
Code language: PHP (php)

5) Backups & restore

Database backup (hot, compressed):

# Backup
docker exec -t timetracker-db pg_dump -U ${POSTGRES_USER} -d ${POSTGRES_DB} \
  | gzip > backup_$(date +%F).sql.gz

# Restore into a fresh DB (stop app first if necessary)
gunzip -c backup_2025-09-01.sql.gz \
  | docker exec -i timetracker-db psql -U ${POSTGRES_USER} -d ${POSTGRES_DB}
Code language: PHP (php)

App logs: stored in the app_logs volume.
Consider shipping logs to your central log stack (e.g., Loki/Promtail or ELK).

6) Updating to a new release

docker compose pull app
docker compose up -d
# The app will run DB migrations automatically at startup.
Code language: PHP (php)

7) Hardening checklist

  • Rotate SECRET_KEY and DB password on first install; store secrets in a password manager.
  • Set firewall rules to expose only 80/443 publicly (not 8080).
  • Keep SESSION_COOKIE_SECURE=true and serve only over HTTPS.
  • Schedule daily DB backups and test restores.
  • Use non-default admin username, strong password policy, and 2FA if added in future releases.

Quick FAQ

Can I run SQLite instead of PostgreSQL?
Yes, but PostgreSQL is recommended for teams and production due to concurrency and robustness.

How do I change the public port?
Adjust the ports: mapping (e.g., - "80:8080") or use the Caddy reverse proxy with standard 80/443.

Does it support multiple teams/clients?
Yes. You can manage multiple clients and projects, set default rates, and track billable vs. non-billable hours.

Is there an API?
Yes, a REST API is available for integrations (see the repo docs/ for endpoints and usage).

Scroll to Top