TimeTagger v26.1.3 Docker Compose 部署指南
以 Ubuntu 服务器实测完成 TimeTagger v26.1.3 的 Docker Compose 部署:官方 ghcr 镜像 root/nonroot 双变体选型、数据目录挂载与重启恢复闭环、凭据 BCrypt 哈希与 .env 单引号防插值、等价 docker run 与升级回滚流程,并沉淀端口冲突与哈希被插值等排障实录。
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/timetagger | v26.1.3(root 变体) | root | /root/_timetagger | 常规自托管,官方默认推荐 |
ghcr.io/almarklein/timetagger | latest-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_BIND | 127.0.0.1:8080 | 监听地址与端口;服务形态必须显式改绑 0.0.0.0:80 |
TIMETAGGER_DATADIR | ~/_timetagger | 数据目录(JWT 密钥 + 各用户 SQLite 库) |
TIMETAGGER_LOG_LEVEL | info | 日志级别(info/debug/warning/error) |
TIMETAGGER_CREDENTIALS | 空 | 登录凭据,用户1:哈希1,用户2:哈希2,逗号或分号分隔 |
TIMETAGGER_PROXY_AUTH_ENABLED | false | 启用反向代理头认证 |
TIMETAGGER_PROXY_AUTH_TRUSTED | 127.0.0.1 | 信任的反代 IP/CIDR 列表 |
TIMETAGGER_PROXY_AUTH_HEADER | X-Remote-User | 携带用户名的请求头名 |
TIMETAGGER_PATH_PREFIX | /timetagger/ | 应用挂载的 URL 路径前缀 |
TIMETAGGER_APP_REDIRECT | false | 根路径是否直接跳转到应用(跳过宣传落地页) |
💡 说明: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 无配置文件可挂载,唯一需要挂载的是数据目录。挂载要点:
- 宿主机目录需提前创建(本文 4.1 前的
mkdir -p);root 变体以 root 运行,宿主机目录属主无需调整; - 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
- 不要只挂
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_* 三件套),让统一认证网关接管登录。
十二、参考链接
- 官方仓库:https://github.com/almarklein/timetagger
- 官方部署文档:https://timetagger.readthedocs.io/en/latest/deploy/
- 官方镜像发布页:https://github.com/almarklein/timetagger/pkgs/container/timetagger
- 凭据哈希生成页:https://timetagger.app/cred
- 本专栏认知篇:《TimeTagger v26.1.3 介绍与选型指南》
- 本专栏配置篇:《TimeTagger v26.1.3 配置参数详解》
- 本专栏 API 篇:《TimeTagger v26.1.3 API 使用详解》