TimeTagger v26.1.3 Docker Compose 部署指南

以 Ubuntu 服务器实测完成 TimeTagger v26.1.3 的 Docker Compose 部署:官方 ghcr 镜像 root/nonroot 双变体选型、数据目录挂载与重启恢复闭环、凭据 BCrypt 哈希与 .env 单引号防插值、等价 docker run 与升级回滚流程,并沉淀端口冲突与哈希被插值等排障实录。

TimeTagger时间追踪自托管项目管理2026/10/24

TimeTagger v26.1.3 Docker Compose 部署指南

目标:用 Docker Compose 把 TimeTagger v26.1.3(开源时间追踪 Web 应用)跑起来,完成官方镜像部署、数据目录挂载持久化、重启恢复闭环验证与常用运维操作。全部命令与回显已在 Ubuntu 22.04 服务器(Docker 29.8.1 + Docker Compose v5.5.1)实测通过,服务以 admin / q1w2e3r4 账号完成登录与 API 联调验证。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS
  • Docker:29.8.1;Docker Compose:v5.5.1(docker compose 子命令形式)
  • 部署对象:TimeTagger v26.1.3(2026-02-02 发布,官方最新稳定版)
  • 官方镜像:ghcr.io/almarklein/timetagger:v26.1.3(实测镜像体积 362MB)
  • 项目目录:/opt/timetagger-compose/
  • 端口:宿主机 8137 → 服务 80(TimeTagger 镜像内默认监听 80)
  • 持久化:./_timetagger 目录 bind mount 到服务内 /root/_timetagger
  • 演示账号:admin,密码 q1w2e3r4(BCrypt 哈希写入配置)

二、镜像选型说明

TimeTagger 官方只通过 GitHub Container Registry(ghcr.io)发布镜像,不提供 Docker Hub 官方镜像(2026-10-24 实查官方 README 与镜像发布页)。可选两条路线:

镜像tag运行身份数据目录适用场景
ghcr.io/almarklein/timetaggerv26.1.3(root 变体)root/root/_timetagger常规自托管,官方默认推荐
ghcr.io/almarklein/timetaggerlatest-nonroot(nonroot 变体)uid 1000/opt/_timetagger安全合规要求运行非 root 的环境

💡 说明:官方 deploy 目录提供 root 与 nonroot 两份 compose 模板(2026-10-24 实查)。root 变体以固定版本 tag(v26.1.3)发布,nonroot 变体官方模板固定使用 latest-nonroot 标签。两个变体的数据目录约定不同(root 为 /root/_timetagger,nonroot 为 /opt/_timetagger),挂载路径必须与所选变体一致。本文以 root 变体 + 固定版本 tag 为主线——自托管服务锁版本是升级可控的基本盘;nonroot 变体的差异在第八章单独说明。

除镜像外还有 PyPI 安装形态(pip install timetagger),适合单机个人使用,本文不展开,可参考《TimeTagger v26.1.3 介绍与选型指南》第五章的快速开始。

三、前置条件

确认 Docker 与 Compose 可用:

docker --version
docker compose version

实测输出:

Docker version 29.8.1, build 4a63305
Docker Compose version v5.5.1

准备登录凭据的 BCrypt 哈希。TimeTagger 不收明文密码,凭据格式为「用户名:BCrypt哈希」,官方提供生成页 https://timetagger.app/cred (页面源码在官方仓库 common/cred.html,可在内网离线打开使用)。本文演示账号 admin / q1w2e3r4 对应的哈希为:

$2a$12$uY4YnNxTH3l3FUtI/pZCIOSyzTph.CeZjqyPs3Sz8WarX0Jd2kly.

规划项目目录:

mkdir -p /opt/timetagger-compose/_timetagger
cd /opt/timetagger-compose

四、部署步骤

4.1 编写 .env(存放凭据)

TimeTagger 的 BCrypt 哈希含 $ 字符,官方模板明确给出两种处理方式:写在 compose 文件里要转义为 $$;写在 .env 文件里用单引号包裹即可。单引号方案不用逐个转义、可读性更好,本文采用它:

cat > /opt/timetagger-compose/.env <<'EOF'
TT_CREDENTIALS='admin:$2a$12$uY4YnNxTH3l3FUtI/pZCIOSyzTph.CeZjqyPs3Sz8WarX0Jd2kly.'
EOF
chmod 600 /opt/timetagger-compose/.env

⚠️ 注意:.env 内的值必须用单引号包裹哈希。双引号或裸写时,Compose 会把 $2a、$12 等 $数字 片段当作变量插值,登录会报 Invalid credentials。chmod 600 收紧权限,因为该文件等价于口令文件。

4.2 编写 docker-compose.yml

cat > /opt/timetagger-compose/docker-compose.yml <<'EOF'
name: timetagger-v26.1.3-stack

services:
  web:
    image: ghcr.io/almarklein/timetagger:v26.1.3
    container_name: timetagger-v26.1.3-web
    ports:
      - "8137:80"
    volumes:
      - ./_timetagger:/root/_timetagger
    environment:
      - TIMETAGGER_BIND=0.0.0.0:80
      - TIMETAGGER_DATADIR=/root/_timetagger
      - TIMETAGGER_LOG_LEVEL=info
      - TIMETAGGER_CREDENTIALS=${TT_CREDENTIALS}
    restart: unless-stopped
EOF

逐项说明:

  • name:显式指定 Compose 项目名,避免多套部署在同一宿主机上因目录同名互相干扰;
  • image:锁定 v26.1.3,升级时只改这一行(见第十章);
  • ports:宿主机 8137 → 服务 80。若 8137 被占用,ss -ltn | grep 8137 先查再换;
  • volumes:数据目录整体 bind mount 到宿主机,SQLite 库文件与 JWT 签名密钥都落在这里,是持久化与备份的唯一对象;
  • TIMETAGGER_BIND=0.0.0.0:80:TimeTagger 自身默认绑定 127.0.0.1:8080(v24.12.2 安全修复后的默认值),在固定映射端口的服务形态里必须显式改绑 0.0.0.0:80 才能被映射出去;
  • TIMETAGGER_CREDENTIALS=${TT_CREDENTIALS}:从 .env 注入凭据。

4.3 启动并验证

cd /opt/timetagger-compose
docker compose up -d
docker compose ps

实测输出:

[+] Running 2/2
 ✔ Network timetagger-v2613-net  Created
 ✔ Container timetagger-v26.1.3-web  Started
NAME                     IMAGE                                   COMMAND                  SERVICE   CREATED          STATUS          PORTS
timetagger-v26.1.3-web   ghcr.io/almarklein/timetagger:v26.1.3   "python -m timetagger"   web       58 minutes ago   Up 58 minutes   0.0.0.0:8137->80/tcp, [::]:8137->80/tcp

启动需要几秒(首次启动要编译前端脚本)。查看启动日志:

docker compose logs --tail 20

实测输出(节选):

[INFO 2026-10-24 08:53:37] Collected 14 assets from /root/timetagger/common
[INFO 2026-10-24 08:53:37] Compiled pscript from dialogs.py
[INFO 2026-10-24 08:53:37] Compiled pscript from dt.py
[INFO 2026-10-24 08:53:37] Compiled pscript from front.py
[INFO 2026-10-24 08:53:37] Compiled pscript from stores.py
[INFO 2026-10-24 08:53:37] Compiled pscript from tools.py
[INFO 2026-10-24 08:53:37] Compiled pscript from utils.py
[INFO 2026-10-24 08:53:37] Collected 13 assets from /root/timetagger/app
...
[INFO 2026-10-24 08:53:38] Server is starting up

看到 Server is starting up 即服务已监听。做三项验证:

# 1. 健康检查端点(无需认证)
curl -s -w ' [%{http_code}]' http://localhost:8137/timetagger/status

实测输出:

ok [200]
# 2. 根路径自动重定向到应用前缀
curl -s -o /dev/null -w '%{http_code} Location=%{redirect_url}\n' http://localhost:8137/

实测输出:

307 Location=http://localhost:8137/timetagger/
# 3. 用配置的凭据换取访问令牌(登录验证)
B=http://localhost:8137/timetagger
curl -s -w ' [%{http_code}]' -X POST \
  --data-binary "$(printf '%s' '{"method":"usernamepassword","username":"admin","password":"q1w2e3r4"}' | base64 -w0)" \
  $B/api/v2/bootstrap_authentication

实测输出(令牌为 200 开头的 HTTP 状态码 + JSON):

{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwaXJlcyI6MTc5NDA0NDc3Miwic2VlZCI6ImlUUVJKWXlPLWJzIn0.pCIW7M_nVao4S0zIWsMxH0e9NAWqOI2afkwASjWQOxE"} [200]

💡 说明:TimeTagger 的应用固定挂在 /timetagger/ 路径前缀下(可用 TIMETAGGER_PATH_PREFIX 修改,详见《TimeTagger v26.1.3 配置参数详解》——该前缀与重定向行为的参数化说明见配置篇相应章节)。浏览器访问 http://<服务器IP>:8137/ 会自动跳转到登录页,用 admin / q1w2e3r4 登录即进入时间线界面。若第 3 步返回 Invalid credentials [403],回到 4.1 检查 .env 引号(见第十一章 Q1)。

五、数据持久化说明

TimeTagger 的全部状态收敛在一个数据目录里(本文挂载到宿主机 /opt/timetagger-compose/_timetagger),首次启动后生成:

find /opt/timetagger-compose/_timetagger -maxdepth 2 | sort

实测输出:

/opt/timetagger-compose/_timetagger
/opt/timetagger-compose/_timetagger/jwt.key
/opt/timetagger-compose/_timetagger/users
/opt/timetagger-compose/_timetagger/users/admin~YWRtaW4=.db

逐项说明:

  • jwt.key:JWT 签名密钥(HS256),首次启动自动生成,43 字节随机数据。该文件是登录态的根:删除或更换后,所有已签发的 webtoken/apitoken 全部失效;
  • users/admin~YWRtaW4=.db:每用户一个 SQLite 库文件,命名规则为 用户名~base64(用户名).db(YWRtaW4= 是 admin 的 base64)。时间记录、报表设置都存在这一个文件里。

备份即拷贝整个目录(建议先 docker compose stop 再拷,保证 SQLite 文件一致性);恢复即把目录放回原位再启动。

重启恢复闭环验证——写入数据后重启服务,确认记录与登录令牌都还在:

docker compose restart
sleep 8
curl -s -w ' [%{http_code}]' http://localhost:8137/timetagger/status

实测输出:

 Container timetagger-v26.1.3-web Restarting
 Container timetagger-v26.1.3-web Started
ok [200]

重启前实例中有 3 条时间记录,用重启前签发的 webtoken 查询:

curl -s -H "authtoken: $WT" 'http://localhost:8137/timetagger/api/v2/records?timerange=0-1999999999' \
  | python3 -c 'import json,sys; d=json.load(sys.stdin)["records"]; print(len(d), "条记录; 样例:", d[0]["ds"])'

实测输出:

3 条记录; 样例: #部署 #TimeTagger 撰写部署指南

数据与令牌均完好,持久化闭环成立。

六、环境变量表

TimeTagger 全部配置通过 TIMETAGGER_* 环境变量注入(无配置文件、无命令行必填项),Compose 场景可用的完整清单如下(默认值取自 v26.1.3 实测):

环境变量默认值说明
TIMETAGGER_BIND127.0.0.1:8080监听地址与端口;服务形态必须显式改绑 0.0.0.0:80
TIMETAGGER_DATADIR~/_timetagger数据目录(JWT 密钥 + 各用户 SQLite 库)
TIMETAGGER_LOG_LEVELinfo日志级别(info/debug/warning/error)
TIMETAGGER_CREDENTIALS空登录凭据,用户1:哈希1,用户2:哈希2,逗号或分号分隔
TIMETAGGER_PROXY_AUTH_ENABLEDfalse启用反向代理头认证
TIMETAGGER_PROXY_AUTH_TRUSTED127.0.0.1信任的反代 IP/CIDR 列表
TIMETAGGER_PROXY_AUTH_HEADERX-Remote-User携带用户名的请求头名
TIMETAGGER_PATH_PREFIX/timetagger/应用挂载的 URL 路径前缀
TIMETAGGER_APP_REDIRECTfalse根路径是否直接跳转到应用(跳过宣传落地页)

💡 说明:Compose 里不配置 TIMETAGGER_BIND 时服务绑定 127.0.0.1:8080,端口映射将无法访问——这是最常见的「部署完打不开」原因。每个变量的取值语义、生效机制与踩坑实录在配置篇逐项展开,本章只给部署所需的最小集。

七、配置参数说明

本文 compose 用到的 4 个变量说明如下:

  • TIMETAGGER_BIND=0.0.0.0:80:镜像内的启动入口是 python -m timetagger,读取该变量决定监听地址。0.0.0.0 表示接受任意网卡的连接,80 与 ports 映射的容器侧端口对应;
  • TIMETAGGER_DATADIR=/root/_timetagger:与 volumes 挂载点保持一致,数据就会落到宿主机 ./_timetagger;
  • TIMETAGGER_LOG_LEVEL=info:常规运维取 info;排障时可临时改 debug 后 docker compose up -d 重建生效;
  • TIMETAGGER_CREDENTIALS=${TT_CREDENTIALS}:引用 .env 中的凭据。多用户写法如 'admin:哈希A,ops:哈希B'(逗号或分号分隔均可,实测多用户同时可登录)。

八、配置文件挂载

TimeTagger 无配置文件可挂载,唯一需要挂载的是数据目录。挂载要点:

  1. 宿主机目录需提前创建(本文 4.1 前的 mkdir -p);root 变体以 root 运行,宿主机目录属主无需调整;
  2. nonroot 变体的差异:镜像以 uid 1000 运行,宿主机目录必须归 uid 1000 所有,否则启动报权限错误:
mkdir -p /opt/timetagger-compose-nonroot/_timetagger
chown -R 1000:1000 /opt/timetagger-compose-nonroot/_timetagger

compose 中对应改为:

    image: ghcr.io/almarklein/timetagger:latest-nonroot
    volumes:
      - ./_timetagger:/opt/_timetagger
    environment:
      - TIMETAGGER_DATADIR=/opt/_timetagger
  1. 不要只挂 users/ 子目录:jwt.key 生成在数据目录根部,只挂子目录会导致密钥落到容器层,重建即丢。

九、等价 docker run

不用 Compose 时,等价的 docker run 命令如下(与第四章 compose 等效):

docker run -d \
  --name timetagger-v26.1.3-web \
  -p 8137:80 \
  -v /opt/timetagger-compose/_timetagger:/root/_timetagger \
  -e TIMETAGGER_BIND=0.0.0.0:80 \
  -e TIMETAGGER_DATADIR=/root/_timetagger \
  -e TIMETAGGER_LOG_LEVEL=info \
  -e TIMETAGGER_CREDENTIALS='admin:$2a$12$uY4YnNxTH3l3FUtI/pZCIOSyzTph.CeZjqyPs3Sz8WarX0Jd2kly.' \
  ghcr.io/almarklein/timetagger:v26.1.3

⚠️ 注意:docker run 场景下凭据哈希也要用单引号包裹,防止 shell 把 $2a$12$... 当变量展开成空串。这与 compose 场景的 $$ 转义、.env 单引号是同一类问题的三种形态。

十、常用运维命令

cd /opt/timetagger-compose

# 查看服务状态
docker compose ps

# 跟踪日志(排障首选)
docker compose logs -f --tail 100

# 重启服务(改环境变量后生效的手段之一)
docker compose restart

# 停止 / 启动
docker compose down    # 数据在 bind mount 里,down 不丢数据
docker compose up -d

升级流程(CalVer + 单文件 SQLite,升级即换镜像重启,无迁移脚本):

# 1. 备份数据目录
tar czf /root/timetagger-backup-$(date +%F).tar.gz -C /opt/timetagger-compose _timetagger

# 2. 修改 compose 中的镜像 tag(如 v26.1.3 → 新版本号),然后:
docker compose pull
docker compose up -d

# 3. 验证
curl -s -w ' [%{http_code}]' http://localhost:8137/timetagger/status
docker compose logs --tail 5

💡 技术拓展:服务升级策略常见的有「就地替换(原地 pull 新镜像重启)」「蓝绿(新旧两套并存切流量)」「滚动(多实例逐个替换)」。TimeTagger 是单进程 + 本地 SQLite 的单实例服务,无多实例共享存储能力,蓝绿与滚动均不适用,就地替换 + 先备份是唯一合理策略;SQLite 数据格式跨版本兼容性历史上未见破坏性变更,回滚即把镜像 tag 改回旧版本再 up -d。

十一、常见问题

Q1:登录一直报 Invalid credentials [403],密码确认没写错?

哈希里的 $ 被 Compose 插值吃掉了。检查三处:.env 文件里的值是否用单引号包裹;compose 文件里若直接写哈希,$ 是否已转义为 $$;docker exec timetagger-v26.1.3-web env | grep CREDENTIALS 看实际注入值是否完整(以 $2a$12$ 开头、以 . 结尾)。实测中双引号包裹的哈希会被拆掉 $2a、$12 片段,导致 BCrypt 校验必败。

Q2:服务起来了,curl http://服务器IP:8137 打不开?

按顺序排查:docker compose ps 确认端口映射存在;确认 compose 里配了 TIMETAGGER_BIND=0.0.0.0:80(缺省绑定 127.0.0.1:8080 时映射出去的服务无监听);curl -s http://localhost:8137/timetagger/status 在宿主机本机应返回 ok,本机通而外部不通则是防火墙问题。

Q3:启动日志报 error while attempting to bind on address ... address already in use?

监听端口冲突。TimeTagger 无 --help 参数,python -m timetagger --help 会直接当服务启动(实测即报此错)。该报错说明服务配置的 TIMETAGGER_BIND 端口已被占用(同一数据卷起了两个实例是最常见原因),ss -ltnp | grep <端口> 找到占用者后调整。

Q4:浏览器访问根路径跳到一个「宣传页」而不是应用?

默认 TIMETAGGER_APP_REDIRECT=false,根路径 307 跳到 /timetagger/ 的落地页,落地页上有进入应用的入口。想跳过落地页直达应用,把 TIMETAGGER_APP_REDIRECT=true 写入 compose 的 environment 后 docker compose up -d 重建(实测根路径随即 307 到 /timetagger/app/)。

Q5:想挂到 Nginx 反代的子路径下?

用 TIMETAGGER_PATH_PREFIX=/子路径/(首尾斜杠可省略,服务端自动补齐,实测配 /tracking 后应用挂到 /tracking/、旧前缀返回 404 only serving at /tracking/)。反代场景建议同时评估代理头认证(TIMETAGGER_PROXY_AUTH_* 三件套),让统一认证网关接管登录。

十二、参考链接