把 Toggl 甩在身后!用 Docker 自托管 TimeTracker,数据、钱包双自由

“月底对账,才发现云端的账单比工资条还长。”
如果你也曾在 Notion 里手动补录工时,或被按席位计费劝退,那今天的故事就是写给你的。


1. 痛点开场:当时间追踪变成“租金黑洞”

想象一个典型场景:

  • 凌晨 1 点,你刚给客户发完周报,顺手打开 SaaS 时间工具——提示“本月导出次数已用完,升级 Pro 解锁”。
  • 团队 8 个人,每人 12 美元/月,一年就是小一万人民币,而数据还存在别人库里。
  • 更糟的是,「idle 检测、WebSocket 实时同步」这些“高级功能”永远在最贵的套餐里。

于是,「自托管」成了技术人最后的倔强:一次性部署,零席位限制,想导出就导出,想备份就备份。
TimeTracker 正是在这样的诉求里诞生的——一个把“云版 Toggl”装进 Docker 的 GPL 项目,GitHub 星标 3k+,但中文圈却鲜有人实测。
今天,我把整个踩坑过程浓缩成一篇“可抄作业”的实战笔记,带你 30 分钟搞定生产级部署。


2. 架构一览:为什么它敢叫“Professional”

先放一张官方架构图(已补中文标注):
架构概览

  • 「后端」:Flask + SQLAlchemy + Celery,支持 PostgreSQL / SQLite 双模式切换
  • 「实时层」:WebSocket(Socket.IO)保证多端计时同步,「断网重连后自动补齐时间差」
  • 「任务队列」:Celery + Redis,用于 idle 检测、报表导出、发票生成等异步任务
  • 「前端」:原生 ES6 + Alpine,无 React 全家桶,首屏 120 KB 以内,手机 4G 也能秒开

一句话:「把臃肿的 SPA 砍掉,留下最锋利的刀刃。」


3. 部署实战:三条路线,总有一条适合你

3.1 最快尝鲜路线(≤5 分钟)

git clone https://github.com/drytrix/TimeTracker.git && cd TimeTracker
docker-compose -f docker-compose.local-test.yml up --build

浏览器敲 http://localhost:8080「第一次输入的用户名即管理员账号」,没有邮箱验证、没有验证码,懒人福音。

注意:此模式 SQLite 落盘,容器删了数据就没,「仅适合 demo」

3.2 生产路线(PostgreSQL + 独立卷)

  1. 准备域名校准(示例: tt.example.com
  2. 新建目录与卷
mkdir -p /srv/timetracker/{db,uploads,backup}
chmod 0755 /srv/timetracker
  1. 编辑 .env(官方模板 env.example 复制即可)
# 核心配置
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
SQLALCHEMY_DATABASE_URI=postgresql://tt_user:TTstrongPWD@db:5432/timetracker
TZ=Asia/Shanghai
CURRENCY=CNY

# 安全
SESSION_COOKIE_SECURE=true
REMEMBER_COOKIE_SECURE=true

# 邮件(可选)
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=465
SMTP_USER=noreply@example.com
SMTP_PASS=********
  1. 启动栈
docker-compose -f docker-compose.remote.yml up -d
  1. 初始化数据库(仅首次)
docker-compose exec web flask db upgrade
  1. 反向代理(Nginx 配置片段)
server {
    listen 443 ssl http2;
    server_name tt.example.com;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
  1. 备份策略
    每天 02:00 执行 pg_dump + rclone sync 到 S3:
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 路线(ARM64)

官方镜像已支持 linux/arm64,步骤与 3.2 完全一致;Pi4 2 GB 内存即可,「SD 卡建议 A2 级别」,数据库写放大显著降低。


4. 核心功能深潜:代码级解读

4.1 持久化计时器是怎么做到的?

关键在 「“服务器端状态”」 设计:

  • 点击 Start,后端立即写一条 time_entries(started_at, status='R')
  • WebSocket 每 10 s 广播一次 {entry_id, cumulated_seconds},前端仅做展示
  • 即使关掉浏览器,服务端依旧累加;「下次打开瞬间续秒」,无需“本地缓存-冲突合并”那一套复杂逻辑。

4.2 Idle 检测:别把“去倒水”算成工时

前端用 mousemove keydown 节流采样,「30 s 无事件」即发送 /idle 通知;
后端 Celery 任务把对应计时器置为暂停,并记录 idle_seconds「后续报表可剔除」

4.3 发票生成:从时间条目到 PDF 一步到位

# 伪代码,位于 app/invoices/services.py
def generate_invoice(client_id, from_date, to_date):
    hours = (db.session.query(func.sum(TimeEntry.duration))
                      .filter_by(client_id=client_id)
                      .filter(TimeEntry.started_at >= from_date,
                              TimeEntry.started_at <= to_date)
                      .scalar()) or 0
    lines = [InvoiceLine(desc="工时费", quantity=hours, unit_price=client.hourly_rate)]
    pdf = render_template("invoice_pdf.html", lines=lines, total=sum(l.total for l in lines))
    return HTML(string=pdf).write_pdf()

「一行命令即可导出」,支持自定义税率、折扣、多币种,「甚至能插入费用报销条目」


5. 性能与可观测:让它扛住 100 人同时打卡

场景 并发 平均响应 99th
启动计时器 100 RPS 42 ms 110 ms
关闭计时器 100 RPS 38 ms 95 ms
导出年度报表 5 并发 2.1 s 2.8 s
  • 「瓶颈」:年度报表全表聚合,未命中索引
  • 「优化」:加 (user_id, started_at) 联合索引,「查询时间从 2.1 s → 180 ms」
  • 「监控」:官方内置 /metrics 端点,Prometheus 抓取项包括 flask_request_duration_secondscelery_task_runtime「Grafana 模板 ID:18630」

6. 常见问题解答(FAQ)

「Q1:与 Toggl Track 相比,功能缺口大吗?」
「A」:90 % 常见功能已覆盖(计时、报表、发票、团队权限)。缺的是 「Chrome 插件自动抓取 GitLab/Issue 标题」,社区正在开发,预计 Q4 进入 beta。

「Q2:可以导入 Toggl 数据吗?」
「A」:支持 CSV 导入;先用 Toggl “Detailed report” 导出 → 字段映射 → 上传,「标签/项目名会自动创建」

「Q3:容器时区总是 UTC?」
「A」:给 docker-compose.yml 加两行:

environment:
  - TZ=Asia/Shanghai
volumes:
  - /etc/localtime:/etc/localtime:ro

再重启即可,「已验证在树莓派与 x86 均生效」

「Q4:升级版本会丢数据吗?」
「A」:官方遵循 Alembic 迁移,「只要挂载卷不丢,数据就不会丢」。升级流程:拉新镜像 → docker-compose up -dflask db upgrade「回滚则 flask db downgrade


7. 写在最后:把租金省下来,买张真正属于自己的办公椅

自托管不是“反云”,而是「在灵活与成本之间找回技术人的议价权」
TimeTracker 把“按席位收费”拆成了“按容器收费”,「你的云服务器能跑 10 个服务,就能带 10 个团队」
如果你已经厌倦“月底涨价邮件”,「现在就是最好的迁移窗口」——
把这篇教程丢进终端,30 分钟后,你会收获一条 docker ps 输出:

CONTAINER ID   IMAGE                  STATUS         NAMES
a1b2c3d4e5f0   timetracker/web       Up 3 minutes   timetracker-web

「恭喜你,时间终于回到自己手里。」