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.

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 omitVIRTUAL_HOST
and setSESSION_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).