OneTimeSecret v0.26.19 Docker Compose 部署指南

OneTimeSecret v0.26.19 官方镜像 Docker Compose 部署实战:OTS+Valkey 双服务编排、环境变量逐项讲解、管理员创建与提权、一次性秘密读写验证、数据持久化备份与 SESSION_COOKIE_SECURE 等四大排障实录。

OneTimeSecretOneTimeSecret阅后即焚密码安全Redis缓存2026/10/26

OneTimeSecret v0.26.19 Docker Compose 部署指南

一、环境信息

项目值
操作系统Debian 12(bookworm,x86_64),Docker Engine + Docker Compose v2
OneTimeSecret 版本v0.26.19(官方镜像 onetimesecret/onetimesecret:v0.26.19)
数据存储Valkey 8(官方镜像 valkey/valkey:8,Redis 协议兼容)
Compose 项目名onetimesecret-v0-26-19-stack(compose 文件显式 name:)
自定义网络ots-net(10.89.57.0/24,external 网络预先创建)
对外端口宿主 8132 → 容器 3000
应用监听容器内 0.0.0.0:3000(Puma 双 worker)
测试账号admin@ots.local / q1w2e3r4(Web 注册 + CLI 提权为 colonel)

OneTimeSecret 的定位、架构与选型对比见《OneTimeSecret v0.26.19 介绍与选型指南》,源码裸机路线见《Debian 12 安装 OneTimeSecret v0.26.19 指南》。本文走官方镜像 + Compose 编排路线:一个 OTS 应用容器 + 一个 Valkey 数据容器,从零启动到完成「创建管理员 → 一次性秘密读写闭环」的全流程实测。

二、前置条件

  1. 一台装有 Docker Engine 24+ 与 Docker Compose v2 的 Linux 主机;
  2. 磁盘剩余空间不少于 3 GB(镜像约 680 MB,加 Valkey 数据与前端产物余量);
  3. 宿主机一个空闲端口(本文用 8132);
  4. 可访问公网拉取镜像(网络受限环境为 Docker 配置镜像加速或 HTTP 代理)。

三、版本与镜像说明

OneTimeSecret 官方维护两处镜像源,tag 与 GitHub Release 一一对应:

  • Docker Hub:onetimesecret/onetimesecret(累计拉取 100K+,2026-10-26 实查);
  • GHCR:ghcr.io/onetimesecret/onetimesecret;
  • 另有 -lite 后缀变体(更精简的构建层),本文用标准 tag。

生产环境锁定具体版本 tag(如 v0.26.19)而非 latest:OneTimeSecret 的 SECRET 根密钥参与秘密加密,v0.26 起启动时校验数据密文与 SECRET 的配对关系(SECRET_VERIFIER_MODE),跨大版本随意升级可能触发数据不可解密保护,升级前先读对应 Release Notes 并备份数据卷。

四、编写 docker-compose.yml

工作目录 /opt/onetimesecret-compose/,创建 docker-compose.yml:

name: onetimesecret-v0-26-19-stack

services:
  valkey:
    image: valkey/valkey:8
    container_name: ots-v0-26-19-valkey
    hostname: valkey
    restart: unless-stopped
    command: valkey-server --appendonly yes --requirepass otsvalkeypass123
    volumes:
      - ./valkey-data:/data
    networks:
      - ots-net

  ots:
    image: onetimesecret/onetimesecret:v0.26.19
    container_name: ots-v0-26-19-app
    hostname: ots
    restart: unless-stopped
    depends_on:
      - valkey
    ports:
      - "8132:3000"
    environment:
      SECRET: 458a4b6cd3aeecc52fb8cd5d28b41cd182b637bfe8162cb6f7cf87daab5131d7
      VALKEY_URL: redis://:otsvalkeypass123@valkey:6379/0
      HOST: 192.168.146.131:8132
      SSL: "false"
      SESSION_COOKIE_SECURE: "false"
      AUTH_REQUIRED: "false"
      COLONEL: admin@ots.local
    networks:
      - ots-net

networks:
  ots-net:
    external: true
    name: ots-net

先创建外部网络(多套自托管服务共用一个网段便于互通与隔离管理,本文实验网段为 10.89.57.0/24):

docker network create --subnet 10.89.57.0/24 ots-net

4.1 环境变量逐项说明

变量本文取值作用与注意事项
SECRET64 位 hex 随机串全系统根密钥,会话签名与秘密加密的材料经 HKDF 从它派生;用 openssl rand -hex 32 生成;一经投入使用绝不能更换,否则既有秘密全部不可解密(v0.26 启动时会检测并告警);务必离线备份
VALKEY_URLredis://:密码@valkey:6379/0数据源连接串;valkey 是 compose 服务名(容器 hostname);密码段对应 valkey 命令行的 --requirepass
HOST192.168.146.131:8132对外站点地址(生成分享链接的域名部分);非 80/443 端口要带端口号
SSL"false"生成链接的 scheme 与 Cookie Secure 语义开关;走 TLS 反代时改 true
SESSION_COOKIE_SECURE"false"纯 HTTP 部署的必填项,详见下文排障实录——生产模式下 SSL=false 不会自动降级 Cookie Secure 标志,不显式关闭会导致登录态无法保持
AUTH_REQUIRED"false"是否强制登录才能使用(Web 侧匿名分享开关,false 允许匿名创建,与官方 SaaS 行为一致)
COLONELadmin@ots.local管理员邮箱标记(/colonel 面板关联);它只做身份标记,不自动提权——首个账户仍需 CLI 提权,见下文

镜像内置健康检查(bin/healthcheck.sh),启动后约半分钟内 docker compose ps 会从 starting 翻转为 healthy。

五、启动与首次初始化

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

实测回显(启动约 1 分钟后):

NAME                  IMAGE                                  COMMAND                  SERVICE   CREATED             STATUS                       PORTS
ots-v0-26-19-app      onetimesecret/onetimesecret:v0.26.19   "bin/entrypoint.sh"      ots       About an hour ago   Up About an hour (healthy)   0.0.0.0:8132->3000/tcp, [::]:8132->3000/tcp
ots-v0-26-19-valkey   valkey/valkey:8                        "tini -- docker-entr…"   valkey    About an hour ago   Up About an hour             6379/tcp

确认 compose 项目名生效(防止多套 compose 栈同名互删):

docker compose config --format json | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])"

实测回显:

onetimesecret-v0-26-19-stack

观察应用启动日志(Puma 双 worker):

docker compose logs ots | grep -E 'Listening|booted'

实测回显:

ots-v0-26-19-app  | [1] * Listening on http://0.0.0.0:3000
ots-v0-26-19-app  | [1] - Worker 0 (PID: 16) booted in 0.0s, phase: 0
ots-v0-26-19-app  | [1] - Worker 1 (PID: 22) booted in 0.0s, phase: 0

首页探活:

curl -s -o /dev/null -w '%{http_code}\n' http://192.168.146.131:8132/

实测回显:

200

六、创建管理员账号

v0.26 无预置管理员,推荐两条路径,本文实测走「Web 注册 + CLI 提权」:

第一步:Web 注册。浏览器打开 http://192.168.146.131:8132/ 进入 Sign up(或直接调注册接口),本文实验账号 admin@ots.local,密码 q1w2e3r4(服务端默认口令策略最短 4 位,该密码可通过校验;生产环境请用强密码)。注册成功的判定:响应跳转登录态,且容器日志不再出现 Truemail 校验错误。

注册接口的坑:参数名必须是 login(Rodauth 风格)而不是 email——用 email 时服务端 Truemail 报 Truemail regex validation for failed(注意 failed 前面是空的,即服务端拿到空邮箱),返回 422「Is that a valid email address?」。表单 UI 会自动处理,脚本化注册时要注意。

第二步:CLI 提权为 colonel。环境变量 COLONEL 只标记管理员邮箱,注册出来的账户默认 role 是 customer,需要用容器内 CLI 提权:

docker exec ots-v0-26-19-app bin/ots customers role promote admin@ots.local --role colonel --force

执行后该账户即具备 /colonel 管理面板访问权。两个易错点实测记录:一是旧语法 --action promote 会报参数错误(v0.26 CLI 是 customers role promote <email> --role <role> 位置参数风格,拿不准先 --help);二是不带 --force 时命令会交互式确认,脚本化场景记得带上。

第三步:签发 API Token(调 REST API 用):

docker exec ots-v0-26-19-app bin/ots apitoken admin@ots.local

实测回显(返回 JSON 节选):

{"record":{"apitoken":"5ujh9vgrlbpzww0tdlvpg0hwn7le9ufui8rdpr27mj6d2hz9lu","active":true}}

该 Token 即 Basic 认证的密码部分(用户名填邮箱),REST API 的完整用法见《OneTimeSecret v0.26.19 REST API 实战指南》。

七、功能验证:一次性秘密全链路

用 v1 API(匿名可用)在 compose 栈上验证核心语义:

KEY=$(curl -s -X POST http://192.168.146.131:8132/api/v1/share -d 'secret=B2-compose-verify-8132&ttl=3600' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['secret_key'])")
echo $KEY
echo '=== 首次读取 ==='
curl -s -X POST http://192.168.146.131:8132/api/v1/secret/$KEY
echo
echo '=== 二次读取 ==='
curl -s -w ' [%{http_code}]' -X POST http://192.168.146.131:8132/api/v1/secret/$KEY

实测回显(创建响应返回 secret_key 与 metadata_url,此处只展示 key):

akuaru2j4ekzairwcume4al6ubxuv5co21qsmxuu3n5xetejwcj13dpsu6pu99
=== 首次读取 ===
{"value":"B2-compose-verify-8132","secret_key":"akuaru2j4ekzairwcume4al6ubxuv5co21qsmxuu3n5xetejwcj13dpsu6pu99","share_domain":"http://192.168.146.131:8132"}
=== 二次读取 ===
{"secret_key":"akuaru2j4ekzairwcume4al6ubxuv5co21qsmxuu3n5xetejwcj13dpsu6pu99","message":"Unknown secret"} [404]

「创建 → 读取 → 二次读取 404」语义链完整,compose 部署即达到生产可用状态。

八、数据持久化与备份

Valkey 数据:compose 文件把 ./valkey-data 挂载为 Valkey 的 /data,启动命令带 --appendonly yes,落盘为 Multi-Part AOF 格式:

ls valkey-data/appendonlydir/

实测回显:

appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest

在途秘密(未读未过期)重启后仍可读取,备份策略就是备份这个目录(或对其做 valkey-cli BGREWRITEAOF 后拷贝)。

SECRET 根密钥:与数据同等重要——数据卷在而 SECRET 丢失,等于全部秘密变成密文废纸。SECRET 与 compose 文件一起纳入加密存储(密码管理器/加密盘),不要与 valkey-data 放在同一台机器的同一个备份里。

升级路径:改 compose 里的镜像 tag → docker compose up -d(只重建 ots 容器,Valkey 不动)→ 观察 docker compose logs -f ots 启动日志中 SECRET verifier 校验结果(warn 级别提示说明 SECRET 与数据配对正常但注意版本差异)。

九、日常运维速查

docker compose ps                 # 健康状态(healthy 为就绪)
docker compose logs -f ots        # 应用日志(Truemail/加密告警都在这里)
docker compose restart ots        # 重启应用(会话在 Valkey,不掉登录态)
docker compose down               # 停栈(保留 valkey-data 目录)
docker compose down -v            # 停栈并清空命名卷(本文用 bind mount,此命令不清 ./valkey-data,需手动删)

十、排障实录

坑 1(本专栏最重要的一个坑):登录后仍匿名 / CSRF 校验 403

现象:容器启动正常、匿名分享正常,但 Web 登录后刷新页面仍是未登录态,或任何写操作报 CSRF token mismatch。

根因:v0.24+ 的生产模式下,SSL=false 不会自动降级会话 Cookie 的 Secure 标志——应用 fail closed 强制给 Cookie 加 Secure,浏览器收到 Secure Cookie 后只在 HTTPS 请求里回传,纯 HTTP 部署的后续请求全部裸奔无会话。这是官方在 .env 模板注释里明确写出的行为(「a deployment that genuinely serves plain HTTP in production must opt out explicitly with SESSION_COOKIE_SECURE=false」)。

修复:compose 环境变量显式加 SESSION_COOKIE_SECURE: "false"(纯 HTTP 部署),重建 ots 容器即可;TLS 反代部署则应设 SSL: "true" 且不要动这个开关。

坑 2:脚本化注册返回 422「Is that a valid email address?」

现象:POST /auth/create-account 传 email=admin@ots.local 报 422,容器日志出现 Truemail regex validation for failed(邮箱位置为空)。

根因:v0.24+ 认证层是 Rodauth,注册参数名是 login 而非 email,传 email 时服务端拿到空邮箱做正则校验自然失败。

修复:参数名改 login。顺带说明:注册成功后系统会尝试发送验证邮件,未配置 SMTP 时日志会报发送失败但不阻断注册流程。

坑 3:登录成功后写操作报 CSRF token mismatch

现象:登录成功,但随后的写请求 403(CSRF token mismatch),重放旧 token 无效。

根因:v0.26 每次登录后轮换 CSRF token,旧 token 立即失效。

修复:每次写操作前从最新响应头(x-csrf-token)取新 token 放入请求头 X-CSRF-Token。脚本化调用建议按「请求 → 读响应头 → 带新 token 重试」的顺序组织。

坑 4:TTL 传 30 天没报错但实际只有 7 天

现象:POST /api/v1/share 传 ttl=2592000(30 天),返回 200 正常创建,但秘密 7 天后过期。

根因:匿名创建受 TTL_MAX_ANONYMOUS(默认 604800 秒 = 7 天)约束,超限不拒绝而是静默钳制到上限。这不是 bug 而是产品设计;要放开就改配置(登录用户可配更高档位),参数细节见《OneTimeSecret v0.26.19 配置参数详解》。