☰
自托管经期追踪实战:数据主权、周期预测与安全备份
2026/10/23 1:30:05 网站建设 项目流程

自托管经期追踪这件事,我前后折腾了小半年。起因很简单:我把过去两年的周期记录从某个手机应用里导出时,发现导出的 CSV 少了三列我当初填得很认真的症状标签,而且客服给的回复是"历史字段已下线"。那一刻我才意识到,健康数据放在别人服务器上,能不能取回、能取回多少,决定权不在我手里。后来我搭了一套跑在自己机器上的经期追踪服务,项目代号叫 Ovumcy,思路就一句话:数据不上云,完全自主掌控。

这套东西能干什么?记录每日的身体状态和经期起止、按历史数据推算下次周期和易孕窗口、用日历视图回看趋势、随时把全量数据导成明文文件。它解决的不是"功能比商业应用更强"的问题——恰恰相反,功能上它朴素得多——它解决的是数据归属问题:谁拥有这些记录、谁能看、能存多久、什么时候删。适合看这篇的人大概分两类:一类是有基本运维能力、愿意花一个周末把服务跑起来的人;另一类是暂时不打算自己搭,但想搞清楚"自托管健康数据"到底意味着什么代价的人。

1. 为什么我把周期记录搬回自家机器

1.1 云端健康数据的三个真实顾虑

第一个顾虑是数据的生命周期不可控。你在应用里点"删除账号",通常只删掉了主库里的行,备份快照、日志、分析仓库里的副本往往还在,什么时候清、清到什么程度,用户看不到也验证不了。健康类数据尤其麻烦,它不像购物记录,删了顶多推荐不准;它的敏感度决定了"残留"本身就是风险。

第二个顾虑是用途边界会漂移。任何一款应用的用户协议都不是一成不变的,版本更新时大多数人点"同意"就过去了。今天它只做记录,明天可能接入了广告归因、健康分析合作方,或者把聚合数据卖给研究机构。这些变化在协议里写得清清楚楚,但没人会逐版对比。自托管的好处在于:这个边界由你自己划,而且没有第三方能单方面改动它。

第三个顾虑是单点故障。账号被盗、服务商跑路、区域服务下线,这些事概率不高,但一旦发生,你损失的不是钱,是几年积累的连续记录——而周期数据恰恰是"连续才有价值"的典型,断三个月,预测精度直接崩。所以我要的不只是"能存",而是"我能把它拷走、能自己恢复"。

1.2 自托管换来的三样东西和失去的三样东西

很多人写自托管只讲好处,我觉得不实在。先把取舍摊开说。

维度自托管得到自托管失去
数据主权数据文件在自己磁盘上,随时可导出、可销毁没有厂商帮你做异地多活
可审计代码、依赖、网络请求全部可见可查需要你自己判断依赖是否可信
定制自由想加字段就加字段,想改算法就改算法没有产品团队替你打磨交互
可用性不依赖任何第三方服务状态停电、硬盘坏、误删都得自己扛
跨端体验浏览器访问,任何设备都能开没有原生小组件、没有系统级提醒
维护成本一次性投入为主定期打补丁、做备份、看日志

我自己的结论是:如果你只想"随手记一下",商业应用更省事;如果你把这份记录当成需要保管五到十年的个人档案,那自托管的账算得过来。因为它本质上不是"应用",是"你自己的数据库加一个薄薄的界面"。

1.3 一个"够用就好"的功能边界

我在设计时做过一次很克制的裁剪,明确列出"不做"的清单:不做社交分享,不做社区内容,不做消息推送营销,不做多租户 SaaS,不做第三方数据上报,不做埋点统计。这些砍掉之后,整个服务的复杂度掉了一个数量级——没有消息队列,没有缓存集群,没有对象存储,一个容器加一个数据目录就完事了。

保留的核心只有四块:日历视图(看周期分布)、每日记录(流量、症状、体温、备注)、预测引擎(下次经期、易孕窗口、置信度)、导入导出(CSV 与全量备份)。这四块里真正有技术含量的是预测引擎和备份恢复,其余都是常规增删改查。所以后面几章我会把重点放在算法参数、数据建模、部署与恢复演练上,界面部分不展开。

2. 架构与选型:把复杂度压到最低

2.1 单机单容器,为什么不做微服务

这套服务的真实负载是什么量级?一个用户,一天最多写三条记录,一年一千条上下,读取集中在早上和晚上各几分钟。峰值并发是 1,不是 1000。在这种量级下引入微服务、消息队列、服务发现,属于典型的负资产——它不会提升任何指标,只会增加故障面和维护时间。

所以我选的是单进程单体:一个应用容器,内含 HTTP 服务、模板渲染、定时任务(每日数据校验与备份触发);一个持久化目录挂载到宿主机;前面放一层轻量网关做 TLS 终止和请求转发。整个部署就是一条启动命令加一个配置文件。

这里有个容易被忽略的细节:单体不等于没有分层。我在代码里仍然分了四层——路由层、业务层、存储层、算法层,只是它们跑在同一个进程里。好处是将来真要多用户,把存储层换掉、把会话改掉就能横向扩;坏处是边界靠自觉维持,容易写着写着就耦合。我的做法是强制算法层纯函数化:输入一个周期数组,输出预测结果,不碰数据库、不碰时间以外的全局状态。这样算法可以单独跑测试,也方便你按自己的理解替换。

2.2 数据存在哪、怎么存

存储选型上我对比过两类方案,结论对个人规模非常明确。

对比项单文件嵌入式数据库独立数据库服务
部署复杂度随应用启动,零额外进程需单独容器、端口、账号、初始化
备份方式单文件快照或在线导出语句需逻辑备份工具或物理备份
事务能力单写者,读多写少场景足够强,支持高并发写
数据量上限几 GB 级别无压力,本例一年不到 1MB无实际上限
迁移成本拷文件即迁移需导数据、改连接串
主要风险误删、磁盘损坏、并发写锁配置错误、版本升级不兼容

我选单文件方案,核心理由是备份即复制。这句话听起来朴素,但它决定了灾难恢复的可行性:你半夜出事,最不想面对的就是"先装一个数据库服务、再想办法把 dump 导进去"这种流程。单文件的话,把备份文件放回目录、改个权限、重启容器,五分钟能恢复。

代价也要说清楚:单写者模型下,长时间写事务会阻塞其他写请求,所以我把所有写操作控制在毫秒级,批量导入用事务包装但限制单批条数;另外开启预写日志模式,让读写不互相阻塞,同时接受"断电可能丢最后一条未落盘记录"这个微小风险——对日记类数据完全可接受。

2.3 加密边界划在哪里

这是自托管健康数据最值得想清楚的一节。加密不是"全都加"就好,加错位置会让系统变得既不好用也不更安全。我把它分成三层来看:

传输层。外部访问一律走 TLS,禁止明文端口对外。内部如果只有单机通信,不必再加一层加密,那是自我安慰。

静态存储层。我用的是宿主机全盘加密加备份文件加密。全盘加密解决"硬盘被物理拿走"这个场景,成本几乎为零,收益很实在。备份文件用对称加密(AES-256-GCM 这类),密码单独记在你脑子或离线密码本里,不放在同一台机器上——否则备份和钥匙一起丢,等于没加密。

应用层字段加密。这个要慎重。把备注、症状这些字段在入库前单独加密,好处是即使数据库文件泄露也读不到明文;坏处有三个:一是搜索和排序彻底失效,你没法"按关键词查找所有记录";二是密钥管理水平瞬间提高,密钥丢失等于数据永久报废;三是迁移和调试成本陡增。我的判断是:单用户、单机、已经全盘加密的前提下,应用层字段加密的边际收益不高,反而显著抬高运维门槛。所以默认关闭。

如果你确实要开,我建议只加密两个字段:自由文本备注、体温之外的隐私标签。结构化字段(日期、流量等级)保持明文,这样统计和预测还能跑。这是一条经验:加密的粒度应该跟着"使用方式"走,不是跟着"敏感程度"走。所有字段都加密的系统,最后往往因为不能查询而被用户放弃。

2.4 技术栈对照表

选型上我没有追求时髦,原则是"五年后我还能看懂并修得动"。

位置选择选择的理由被否决的方案
后端语言主流服务端语言,单二进制或成熟运行时生态全、容器体积可控、调试工具成熟小众语言,五年后可能找不到维护者
模板渲染服务端渲染无前端构建链,改一行刷新即见效单页应用,构建链路维护成本高
前端交互少量原生脚本页面只有日历和表单,无需框架重型前端框架,引入打包与版本升级负担
存储单文件嵌入式数据库备份即复制独立数据库服务,运维成本不匹配规模
对外入口轻量网关做 TLS 与转发证书管理集中,应用不碰证书应用直接监听公网端口
证书自动签发与续期避免忘记续期导致中断手动上传证书文件
备份定时导出加密快照可离线验证直接拷贝运行中数据文件

这套组合的总体积可以压到几十 MB,内存占用几十 MB 量级,一台最低配的云主机或家里的旧笔记本都能跑。

3. 核心:周期预测怎么算才不瞎猜

3.1 加权平均周期,别用简单平均

大部分简易实现用的是"所有历史周期长度的算术平均"。这个做法有个明显缺陷:三年前一段不规律时期的数据会持续拉偏预测,而最近几个月的变化反而被稀释。人的周期会随年龄、作息、体重、季节变化而缓慢漂移,越近的数据权重应该越高。

我用的是指数衰减加权平均。设最近 N 次完整周期(从一次经期开始日到下一次开始日的天数)为 c₁, c₂, …, c_N,按时间从远到近排列,权重定义为:

wᵢ = λ^(N−i),其中 λ 取 0.8

预测周期长度:

L̂ = Σ(wᵢ · cᵢ) / Σ(wᵢ)

拿一组具体数字算一遍。假设最近 6 次周期依次是 30、28、31、27、29、26 天(从远到近)。λ=0.8 时,权重依次为:

  • 0.8⁵ = 0.3277
  • 0.8⁴ = 0.4096
  • 0.8³ = 0.5120
  • 0.8² = 0.6400
  • 0.8¹ = 0.8000
  • 0.8⁰ = 1.0000

权重和 = 3.6893。加权和 = 30×0.3277 + 28×0.4096 + 31×0.5120 + 27×0.6400 + 29×0.8000 + 26×1.0000 = 9.831 + 11.469 + 15.872 + 17.280 + 23.200 + 26.000 = 103.652。L̂ ≈ 28.1 天。

简单平均是多少?(30+28+31+27+29+26)/6 = 28.5 天。差了 0.4 天——看着不大,但这是在没有明显趋势的情况下。如果最近三个月周期从 30 天缩到 26 天,简单平均会给出 28 天以上,而加权平均会明显往 26 靠。预测日差一天,对"提前准备"这件事的实际体验影响是很大的。

λ 的取值是一个可调参数:λ 越小,对近期变化越敏感,但也越容易被偶发的异常周期带偏;我的默认值是 0.8,等于把最近一次周期的权重设为最远一次的三倍左右。如果你记录得很规律,可以调到 0.9 让曲线更平滑;如果你正处于周期波动期(产后、停药后),调到 0.7 会更贴合。

3.2 黄体期恒定假设与易孕窗口估算

预测下次经期开始日很简单:最近一次经期开始日 + L̂,四舍五入到天。

排卵窗口的估算要复杂一些,业界通用的是一个简化模型:假设黄体期长度相对恒定,约 14 天(个体差异通常在 12–16 天之间),那么排卵日约等于"下次经期预计开始日 − 14"。围绕排卵日往前推 5 天、往后推 1 天,得到所谓易孕窗口,共 7 天。

这个模型的局限必须讲明白,否则会误导人:

  • 黄体期恒定只是近似。同一个体不同周期之间也有一两天波动,用固定 14 天会引入误差。
  • 它只能给出统计意义上的窗口,不能用于避孕决策。这是我在界面上专门加了一行提示的原因——算法预测和医学判断是两件事。
  • 周期不规律时窗口会失去意义。如果历史周期标准差超过 4 天,窗口宽度可能要拉到 10 天以上,这时候"窗口"这个词本身就是误导。

所以我在实现里做了一件事:把窗口宽度和置信度绑定。窗口不是固定的 ±5/+1,而是随标准差扩张:

  • 标准差 < 1.5 天:窗口 = 排卵日 −5 到 +1
  • 标准差 1.5–3 天:窗口 = 排卵日 −6 到 +2
  • 标准差 3–4 天:窗口 = 排卵日 −7 到 +3
  • 标准差 > 4 天:不显示窗口,只显示"数据波动较大,暂不给出窗口估算"

3.3 置信度分级:什么时候该拒绝预测

这是我认为比算法本身更重要的一节。一个不负责任的追踪工具,会在只有两条记录时也给你一个斩钉截铁的预测日。我的原则是:数据不足就不预测,波动过大就降级提示。

先算标准差。用加权平均算出的 L̂,再算加权标准差 σ:

σ = sqrt( Σ(wᵢ · (cᵢ − L̂)²) / Σ(wᵢ) )

分级规则我用一张表固定下来,避免每次凭感觉:

条件置信度界面表现
完整周期数 < 2不预测提示"至少需要 2 个完整周期"
完整周期数 2–3 且 σ ≤ 2低显示预测区间而非单日
完整周期数 ≥ 4 且 σ ≤ 1.5高显示预测日 + 窗口
完整周期数 ≥ 4 且 1.5 < σ ≤ 4中显示预测区间(±σ 取整)
σ > 4不预测提示波动较大,建议补录数据
距上次开始日超过 90 天不预测提示数据可能中断,需确认

还有两个特殊场景要处理:剔除异常值和处理中断。异常值方面,如果有一次周期长度超过 L̂ 的 2 倍或短于一半,多半是漏记导致的(把两次经期记成了一次,或中间补录错位),我会把它标记为可疑并在计算中降权,而不是静默使用。中断方面,如果相邻两次记录间隔超过 90 天,中间那段大概率是没记,我直接断开这个周期,不参与平均——否则会算出一个 100 多天的"周期",把后续预测全带偏。

3.4 数据表结构与索引设计

数据模型我改过三版,最终落成四张表。核心思路是:周期是派生数据,日志是原始数据。不要手动维护"周期表",让它由日志推导出来,否则两者一定打架。

-- 用户表:单用户场景也保留,方便将来加家庭成员 CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, -- 强哈希,含盐 created_at TEXT NOT NULL, tz TEXT NOT NULL DEFAULT 'Asia/Shanghai' ); -- 每日日志:一天一条,date 用本地日期字符串,不用时间戳 CREATE TABLE daily_logs ( user_id INTEGER NOT NULL, log_date TEXT NOT NULL, -- 格式 YYYY-MM-DD flow INTEGER DEFAULT 0, -- 0 无 / 1 点滴 / 2 少 / 3 中 / 4 多 is_start INTEGER DEFAULT 0, -- 是否经期首日(人工确认) symptoms TEXT, -- JSON 数组,标签集合 temperature REAL, -- 基础体温,摄氏度 note TEXT, -- 自由文本 updated_at TEXT NOT NULL, PRIMARY KEY (user_id, log_date) ); -- 派生周期表:可随时由 daily_logs 重建 CREATE TABLE cycles ( user_id INTEGER NOT NULL, start_date TEXT NOT NULL, end_date TEXT, length INTEGER, -- 到下一次 start 的天数 excluded INTEGER DEFAULT 0, -- 标记异常,不参与计算 PRIMARY KEY (user_id, start_date) ); CREATE INDEX idx_logs_date ON daily_logs (user_id, log_date DESC); CREATE INDEX idx_cycles_start ON cycles (user_id, start_date DESC);

几个关键决策值得解释。日期用文本存 YYYY-MM-DD,不用时间戳。这是踩过坑之后改的:早期我用时间戳,结果服务器在 UTC、用户在 UTC+8,晚上 11 点记的"今天"存进去变成"昨天",日历视图整体错位一天。改成"用户本地日历日"的文本之后,这个问题从根上消失了。代价是不能直接做跨时区的时刻运算——但周期记录根本不需要时刻,只需要"哪一天"。

is_start 由人工确认,不靠算法猜。最开始我尝试用流量值自动判断经期首日(比如流量从 0 变正数即为首日),结果点滴出血、非经期出血、漏记都会造成误判。后来改成:算法给一个建议起点,用户点一下确认。多一次点击,换来的是整条数据链可靠。

cycles 表是缓存,不是真相。任何一次日志修改,都会触发该用户从最近三个月开始重算周期,然后覆盖 cycles 表。这样即使手工改错了,删掉 cycles 表数据再重算就能修回来。

4. 部署实操:从零把服务跑起来

4.1 主机与环境准备

主机要求不高:1 核 1G 足够,磁盘留 10G 给数据和备份。系统选长期支持版本,别追新。真正需要注意的是三件事。

第一,磁盘加密。云主机多数支持在创建时开启全盘加密;家里的小机器则建议用系统自带的加密卷功能。这一步在创建时做,事后补做很麻烦。

第二,时间同步。预测依赖日期,日期依赖时钟。如果时钟漂移,日志日期会错。开系统自带的时间同步服务即可,但要确认它真的在跑——我见过容器里时钟偏移十几分钟导致备份文件名重复覆盖的情况。

第三,用户与权限。不要用管理员账号跑服务。新建一个专用系统用户,数据目录归它所有,权限设成 700。容器里也指定非 root 用户运行,映射宿主机的 UID,避免挂载目录出现属主错乱。

4.2 配置项逐条解析

配置文件我用了 YAML 加环境变量覆盖的方式,敏感项只从环境变量读。

server: listen: "0.0.0.0:8080" base_url: "https://your-domain.example" session_timeout_minutes: 480 max_login_attempts: 5 lockout_minutes: 30 storage: data_dir: "/app/data" db_file: "ovumcy.db" wal_mode: true busy_timeout_ms: 5000 prediction: lambda: 0.8 min_cycles_for_prediction: 2 luteal_phase_days: 14 max_gap_days: 90 outlier_ratio: 2.0 backup: enabled: true schedule: "0 3 * * *" keep_daily: 7 keep_weekly: 4 target_dir: "/app/data/backups" encrypt: true privacy: log_level: "warn" log_request_body: false anonymize_client_ip: true

逐条说几个容易被忽略的。

base_url必须填对外真实地址。登录跳转、导出链接都依赖它,填错会导致登录后被踢回登录页——这个坑我蹲了半小时才反应过来,因为日志里一切正常。

busy_timeout_ms是单文件数据库的关键参数。写冲突时如果立刻报错,用户体验就是"点击保存没反应"。设成 5000 毫秒,冲突时排队而不是失败。太小会报错,太大则会挂起,5 秒是个合理值。

max_gap_days: 90决定什么时候断开周期链。数值越大越宽容,但会把长期未记录导致的虚假周期算进去。90 天覆盖了绝大多数正常周期波动,又不至于把半年的空窗当成一个周期。

log_request_body: false和anonymize_client_ip: true属于隐私项。默认关闭请求体日志,因为表单里可能有备注文本,落进日志就等于明文存了一份。客户端 IP 做截断或哈希,方便排查异常登录又不留完整地址。

密钥这样生成和注入,不要把密钥写进配置文件:

# 生成 32 字节随机密钥,输出 base64 head -c 32 /dev/urandom | base64 # 写入仅当前用户可读的环境文件 umask 077 cat > /etc/period-tracker.env <<'EOF' APP_SECRET_KEY=<上一步的输出> BACKUP_ENCRYPT_KEY=<另一个独立生成的密钥> EOF

这里有一条硬规矩:应用密钥和备份加密密钥必须是两个不同的值。共用一把钥匙,意味着备份文件泄露的同时应用也被攻破;分开之后,至少还有一道门槛。两个密钥都要在离线介质上留一份,我用的是一张纸加一个离线密码本,听起来很原始,但它不会因为硬盘坏而消失。

启动命令大致长这样:

容器运行时 run -d \ --name period-tracker \ --restart unless-stopped \ --user 1000:1000 \ -p 127.0.0.1:8080:8080 \ --env-file /etc/period-tracker.env \ -v /srv/period-tracker/data:/app/data \ <你的镜像地址>

注意-p 127.0.0.1:8080:8080前面的那个127.0.0.1。这是一个安全细节:只把端口绑在回环地址上,外网无法直连,必须经过前置网关。很多人写-p 8080:8080,等于把应用裸奔在公网上,后面的 TLS 和认证全白做。

4.3 对外入口与证书

对外只开两个端口:80 用于跳转和证书验证,443 用于正式服务。前置网关负责三件事:TLS 终止、请求转发到本机 8080、加几个安全响应头。

# 前置网关的站点配置片段(字段名按你用的组件调整) server: listen: 443 ssl http2 server_name: your-domain.example ssl_certificate: /etc/certs/fullchain.pem ssl_certificate_key: /etc/certs/privkey.pem ssl_protocols: TLSv1.2 TLSv1.3 add_header Strict-Transport-Security "max-age=31536000" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "no-referrer" always; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 8m; }

X-Frame-Options: DENY防的是点击劫持,Referrer-Policy: no-referrer防的是从外部链接跳进来时把路径泄露出去——路径里可能有记录 ID,这也算隐私。

证书用自动签发与续期,别手动传。手动传的证书一定会忘,忘了一定会过期,过期那天你正好想记录。续期任务要验证真的能跑通,我习惯在部署完立刻手动触发一次续期演练,确认流程通畅。

4.4 首次启动验证与冒烟测试

容器起来不代表服务可用,我固定跑一套五分钟的检查。

第一步看日志有没有报错,第二步访问健康检查端点,第三步走完整注册登录,第四步写一条今天的记录并刷新页面确保持久化,第五步导出一次数据看文件能否打开。这五步里最容易出问题的是第四步——写进去了但刷新就没了,通常是数据目录权限不对,容器写的其实是临时层,重启即丢。

# 检查数据是否真的落在宿主机上 ls -l /srv/period-tracker/data/ # 应该看到数据库文件与 -wal / -shm 附属文件 # 检查容器内的写入用户 容器运行时 exec period-tracker id # 健康检查 curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/healthz

还有一个细节:-wal和-shm文件存在是正常的,说明预写日志模式在工作。备份的时候不能只拷主库文件,否则可能丢最近的事务。正确做法是用数据库自带的在线导出语句,或者先做一次检查点再复制。

4.5 备份与恢复演练

备份不做恢复演练,等于没备份。我的策略是每天凌晨全量加密快照,保留 7 份日备、4 份周备,且每周把一份快照同步到另一台机器或外部介质上。

# 在线导出,生成一致性快照(不锁库) 容器运行时 exec period-tracker sh -c \ 'db-cli "$DATA_DIR/ovumcy.db" "VACUUM INTO '\''/app/data/backups/snap-$(date +%F).db'\''"' # 加密压缩 tar -czf - -C /srv/period-tracker/data/backups snap-$(date +%F).db \ | openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt \ -pass file:/etc/backup-pass \ -out /srv/backups/period-$(date +%F).tar.gz.enc # 校验能否解密并解包(关键一步,别省) openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \ -pass file:/etc/backup-pass \ -in /srv/backups/period-$(date +%F).tar.gz.enc \ | tar -tzf - > /dev/null && echo "备份可解密可解包"

恢复流程要写下来贴在旁边,因为真出事的时候人是慌的:停容器、把当前数据目录改名留档、解出备份文件放进数据目录、修正属主、启容器、登录核对最近七天的记录条数和日期。核对条数这一步别偷懒,我就遇到过一次备份文件是空的——定时任务跑在一个没有正确挂载数据目录的临时容器里,导出的"快照"只有表结构没有数据,直到两个月后演练才发现。

5. 安全加固清单:自托管最容易漏的几处

5.1 暴露面收敛

自托管服务被攻破,九成不是因为代码有漏洞,而是因为暴露了不该暴露的东西。所以第一件事是数清楚你到底开了几个端口。

必要的只有 443 和 80。SSH 只允许密钥登录、禁用密码、限制来源地址或改到非默认端口。数据库端口绝不对外,容器网络内部通信即可。管理后台如果有独立端口,也不对外。

再进一步,如果你的访问来源相对固定(比如只在自己家和公司网络用),可以在防火墙上做来源白名单。这条能挡掉绝大部分自动化扫描。移动场景下网络会变,那就退一步,至少把登录失败锁定、双因素认证打开。

加固项做法不做会怎样
端口收敛仅 443/80 对外,应用端口绑回环应用被直接访问,绕过网关与证书
SSH密钥登录、禁用密码、限制来源被暴力破解,获得系统权限
数据库不对外、仅本机套接字数据被直接拖走
管理路径改默认路径或加来源限制被扫描器命中
登录防护失败锁定 + 双因素弱密码被撞库
依赖更新每月检查一次安全公告已知漏洞长期暴露

5.2 认证与会话

单用户场景很容易把认证做得敷衍,比如一个写死的密码、一个永不过期的会话。这在公网环境里等于没有认证。

我的做法是:密码用强哈希加盐存储,内存开销参数调到能接受的程度;会话用随机生成的令牌,存服务端,有效期 8 小时,滚动续期但设绝对上限;登出时服务端删除会话,不只是删客户端 cookie。登录失败计数按用户名加来源 IP 组合统计,连续 5 次锁定 30 分钟——锁定时长别设太长,否则自己输错三次就被关在门外,我吃过这个亏。

双因素认证如果实现,务必提供一次性恢复码,并且恢复码要能离线保存。否则手机丢了等于账号永久锁死,这时候你只能去改数据库,而改数据库又是另一个风险点。

5.3 日志的隐私边界

日志是隐私泄露的重灾区。默认配置下,网关会记录完整请求路径、来源地址、User-Agent。路径里可能带记录 ID,来源地址能定位到人,这些都不该长期保留。

我的配置是:日志级别设 warn,不记请求体;来源地址做截断或哈希;保留期 7 天,超期自动清理;错误日志里如果带了用户输入,脱敏后再写。另外,应用自身的业务日志不要记录任何具体的健康数据——"用户 1 更新了 2024-06-01 的记录"这种可以,"流量=4,症状=腹痛"这种不行。

再补一条容易被忘的:容器标准输出也是日志。如果你用容器日志收集,记得给容器设日志轮转上限,否则一年后一个几十 GB 的日志文件躺在磁盘上,里面还可能有敏感片段。

6. 常见问题与排查实录

6.1 容器起来就退出,日志里只有一行

最典型的三种原因。第一,数据目录权限不对,应用无法创建数据库文件,启动即失败。表现为日志里出现"unable to open database file"之类。解决:确认宿主机目录属主与容器内运行用户 UID 一致。

第二,环境变量缺失。密钥没注入或者变量名拼错,应用在启动时校验失败直接退出。这类问题日志通常会明确指出缺哪个变量,但如果你把日志级别调得太低,可能什么都看不到——排查时临时把级别调到 info。

第三,端口冲突。宿主机上已经有别的服务占了 8080。这时候容器可能不会退出而是启动失败重试,--restart unless-stopped会导致它反复重启,看起来像"起来了又挂了"。

排查顺序我固定为:看日志 → 看退出码 → 看挂载 → 看端口。退出码 1 一般是配置或权限,137 是被强制终止(多半内存不足),143 是正常收到终止信号。

6.2 时区与跨日边界导致的记录错位

症状是:晚上十一点半记的"今天",第二天看日历标在了前一天。原因前面提过——用了时间戳并做了时区转换。

修复分两步。第一步,把日期字段改成"本地日历日"文本,写入时由前端或服务端按用户配置的时区直接算出 YYYY-MM-DD,不做 UTC 转换。第二步,历史数据迁移时按原记录的时区重新计算日期,别直接截取 UTC 日期的前 10 位——那样会把一批记录整体前移或后移一天。

迁移脚本要先在副本上跑,比对迁移前后的记录条数和日期分布。我当时的做法是导出两份 CSV,用 diff 比对,确认差异全部落在预期的边界时段内,才敢在正式库上执行。

6.3 预测结果和手机应用差一天

差一天非常常见,原因通常有三个,按概率排序。

一是算法不同。商业应用多用简单平均或固定周期,我这边是加权平均,最近几次周期的权重更高,如果最近周期偏短,预测就会提前一天。这不代表谁错了,只是模型不同。

二是"从哪天开始算"的定义不同。有的应用把经期首日定义为流量达到某个等级的第一天,有的定义为用户手动标记的那天。我这边是人工确认,所以如果你的实际首日和标记日差一天,整条链都会差一天。

三是取整方式不同。加权平均算出 28.4 天,向下取整是 28,四舍五入是 28,但四舍五入 28.6 就变 29。我用的是四舍五入到最近整数,然后加在最近一次首日上。如果你想对齐,可以在配置里把取整方式改成固定值,代价是长期看会有累积偏差。

6.4 备份能恢复,但数据不对

这个坑最阴险,因为"能恢复"会给你虚假的安全感。我遇到过三种情况。

第一种,备份的是表结构不是数据。原因是用了一个空的临时容器执行导出,挂载路径没对上。检查方法:解开备份后直接查记录数,不要只看文件大小。

第二种,恢复了旧版本但没做数据迁移。应用启动时如果版本号高于数据库版本,会执行迁移;如果跳过了迁移直接读,字段缺失会导致部分数据被默认值填充。解决:恢复后必须用同版本启动,让它跑完迁移。

第三种,只恢复了主库文件,没恢复预写日志。这种情况下最近几条事务丢失。所以恢复一定要用完整快照,而不是直接拷运行中的主库文件。

6.5 常见问题速查表

现象最可能原因快速处置
容器反复重启端口冲突或权限错误看退出码,检查挂载目录属主
页面能开但登录后跳回登录页base_url 配错改成对外真实地址
保存后刷新数据消失数据目录没挂载到宿主机检查挂载路径与写权限
日历整体偏移一天时区处理错误日期改存本地日历日
预测一直提示数据不足周期未被识别为完整周期检查 is_start 标记与间隔
备份文件很小导出时库为空或路径错解开后查记录数验证
证书到期告警自动续期未生效手动触发续期并查日志
磁盘持续增长日志无轮转设置容器日志上限与清理策略
记录写入偶发失败数据库写锁等待超时调大 busy_timeout,缩短事务
导出 CSV 中文乱码编码不是 UTF-8导出时指定 UTF-8 并加 BOM

7. 尾声:几条只有跑过一段时间才明白的经验

第一,自托管的真正成本不在搭建,在维护。搭一次一个周末,维护是每月半小时乘以好几年。如果你算不清这笔账,不如先用商业应用,等哪天真的觉得数据需要自己保管了再动手。

第二,备份的价值在恢复演练,不在备份本身。我建议每季度挑一个周末,真的把备份恢复到另一台机器上,登录进去翻一翻最近的记录。演练过三次之后,你会对自己的方案有完全不同的信心。

第三,功能越少,活得越久。我最初还想过加图表分析、加多设备同步、加提醒推送,全部砍掉之后,这套东西反而一直在跑。一个人维护的项目,最大的敌人不是需求不够,是需求太多。

第四,别把预测当结论。算法给的是基于历史数据的统计估计,它不知道你这个月出差熬夜、也不知道你换了药。界面上那句"仅供参考"不是免责声明,是实话。

如果后面还要扩展,我会优先做两件小事:一是把导出格式做成通用的日历事件格式,方便导入其他工具;二是加一个"数据健康度"面板,直观告诉你哪些月份记录缺失、哪些周期被标记为异常。这两个功能都不增加隐私风险,但实实在在提升长期可用性。至于多用户、移动端原生应用这些,等真有人需要再说。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询