This commit adds the first complete local-network deployment path for the project. It normalizes the runtime contract around a fixed container listener on 0.0.0.0:10000, binds the published compose port to 127.0.0.1, and keeps the image/build workflow aligned with the released container image. It also introduces an installation script, an nginx reverse-proxy template, and a safer SQLite backup flow based on sqlite3 .backup with retention and optional rclone upload support. Deployment-oriented configuration has been consolidated into .env.example, repository-local .env files are now ignored, and the deployment scripts are executable. In addition, the frontend mixed-content issue is fixed by switching the stylesheet reference to a root-relative static path, with tests updated to cover the regression. README guidance has been expanded to document the new install, nginx, backup, and restore conventions.
14 KiB
Moving Helper
这是一个面向可信家庭内网环境的小型搬家记录工具,当前采用轻量技术栈:
- FastAPI
- Jinja2 服务端渲染
- SQLAlchemy
- SQLite
- Pillow
- pytest / FastAPI TestClient
- Docker / Docker Compose
项目目标是小而稳、容易继续扩展。它不是企业平台,也不是复杂运维系统,重点是本地开发简单、容器部署稳定、数据持久化清楚、后续几个月后自己回来看也能快速接上。
当前已支持
- 固定 3 级结构:
Box -> Item -> SubItem - Box / Item / SubItem 基础 CRUD
- Box / Item / SubItem 单图上传、替换、删除、展示
- Box / Item / SubItem 全局搜索
- Docker / Compose 长期运行
- SQLite 数据持久化
- 基础自动化测试
当前数据模型
这个项目不是无限树结构,而是固定最多 3 级:
BoxItemSubItem
关系如下:
- 一个
Box包含多个Item - 一个
Item属于一个Box Item通过is_container区分是否为“小容器”- 只有
is_container = true的Item才允许拥有SubItem SubItem是最后一级,不允许继续向下嵌套
结构固定为:
Box
└── Item
└── SubItem
图片能力说明
图片系统保持简单直接:
Box最多支持 1 张图片Item最多支持 1 张图片SubItem最多支持 1 张图片- 支持上传、替换、删除
- 不支持多图
图片主要用于帮助识别物品、提高浏览效率、方便手机拍照后直接附加到记录中。它不是原图归档系统。
上传图片后,系统会使用 Pillow 做统一处理:
- 读取上传图片
- 去除 EXIF 元数据
- 转换为 JPEG
- 按最长边缩放到不超过
1600px - 使用约
80质量保存 - 将处理后的 JPEG 二进制直接写入 SQLite
BLOB
同时还会记录:
image_mime_typeimage_widthimage_height
图片访问路由例如:
/boxes/{id}/image/items/{id}/image/subitems/{id}/image
全局搜索
当前支持一个轻量的全局搜索页:
- 路由:
/search - 方式:
GET /search?q=关键词
搜索范围包括:
Box.nameBox.noteItem.nameItem.noteSubItem.nameSubItem.note
当前使用 SQLite 上的简单模糊匹配,不引入外部搜索引擎或复杂全文系统。
搜索结果会显示:
- 对象类型:
Box / Item / SubItem - 名称和备注
- 归属路径
- 对
Item展示所属Box - 对
SubItem展示所属Item和Box - 如果对象已有图片,会显示一个小缩略图
当前未实现
这一阶段仍然没有实现以下内容:
- 多图上传
- OCR
- AI 识别物品
- 图片标签
- 图片分类
- 登录 / 鉴权
- 标签系统
- 前后端分离
- 复杂 UI
项目结构
.
├── app
│ ├── __init__.py
│ ├── config.py
│ ├── db.py
│ ├── images.py
│ ├── main.py
│ ├── models.py
│ ├── static
│ │ └── style.css
│ └── templates
├── data
├── scripts
│ ├── backup_db.sh
│ └── deploy.sh
│ ├── install.sh
│ └── nginx
│ └── moving-helper.nginx.template
├── tests
├── .dockerignore
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── pytest.ini
├── README.md
└── requirements.txt
轻量配置
项目通过环境变量支持以下部署时真正需要关心的配置:
HOST_DOMAINSSL_PATHAPP_DIRBACKUP_DIRBACKUP_REMOTEAPP_PORTDATA_DIRDATABASE_URLCOMPOSE_PROJECT_NAME
推荐从示例文件开始:
cp .env.example .env
默认值如下:
HOST_DOMAIN=moving-helper.lan
SSL_PATH=/etc/acme.sh/$HOST_DOMAIN
APP_DIR=$HOME/.local/share/moving-helper
BACKUP_DIR=$HOME/.local/backup/moving-helper
BACKUP_REMOTE=
APP_PORT=10000
DATABASE_URL=sqlite:////app/data/app.db
DATA_DIR=./data
COMPOSE_PROJECT_NAME=moving-helper
说明:
- 容器内应用固定监听
0.0.0.0:10000 APP_PORT只控制宿主机暴露端口,nginx 默认反代到这个端口APP_DIR是安装脚本复制 compose、.env、备份脚本等运行资产的目标目录DATA_DIR默认为相对路径./data,安装后会相对于APP_DIR解析SSL_PATH由用户自行准备证书目录,安装脚本不会签发证书.env会被 shell 脚本直接source,请保持 shell 兼容写法
本地开发模式
推荐使用本地 Python venv 开发和调试。
1. 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate
2. 安装依赖
pip install -r requirements.txt
3. 启动开发服务器
uvicorn app.main:app --reload --host 0.0.0.0 --port 10000
访问:
http://localhost:10000
本地默认数据库位置:
./data/app.db
Docker 运行方式
Docker / Compose 是这个项目面向长期运行环境的方式。
当前 compose 同时保留了:
image:固定指向code.wanderingbadger.dev/tliu93/2026-moving-helper:latestbuild:用于本地开发时从当前代码构建镜像
当前部署约定已经收敛为:
- 容器内应用固定监听
0.0.0.0:10000 - compose 固定使用
user: 1000:1000 - 宿主机仅在
127.0.0.1:${APP_PORT}暴露后端端口 - SQLite 固定写入容器内
/app/data/app.db
首次准备
cp .env.example .env
mkdir -p data
启动 / 更新:本地代码构建
docker compose up -d --build
这个模式会使用当前仓库代码重新构建镜像,适合本地开发、调试或尚未发布 tag 的阶段。
启动 / 更新:直接拉取已发布镜像
docker compose pull
docker compose up -d
查看状态
docker compose ps
查看日志
docker compose logs -f web
访问:
http://localhost:10000
Compose 配置说明
当前 docker-compose.yml 保持尽量简单:
- 固定镜像地址为
code.wanderingbadger.dev/tliu93/2026-moving-helper:latest - 宿主机默认仅在
127.0.0.1:10000暴露容器10000 restart: unless-stopped- 容器固定使用
1000:1000 - 宿主机
DATA_DIR挂载到容器内/app/data - SQLite 默认写入
/app/data/app.db
因此同一个 compose 文件可以覆盖两种使用方式:
- 本地开发容器:
docker compose up -d --build - 远端部署发布镜像:
docker compose pull && docker compose up -d
自动化部署
这个项目现在额外提供一个面向本地网络环境的最小安装脚本:
sh scripts/install.sh
安装脚本会执行:
- 检查项目根目录下是否存在
.env - 读取
.env - 把
docker-compose.yml、.env和渲染后的backup_db.sh复制到APP_DIR - 用
HOST_DOMAIN、SSL_PATH、APP_PORT渲染 nginx 配置 - 写入
/etc/nginx/sites-available/moving-helper-nginx - 创建到
/etc/nginx/sites-enabled/的符号链接 - 执行
nginx -t并 reload nginx - 在
APP_DIR下执行docker compose pull和docker compose up -d - 为当前用户写入每日
02:10的 backup cron
其中以下步骤需要 root 或 sudo:
- 写入 nginx 配置
- 执行
nginx -t - reload nginx
如果 .env 不存在,脚本会直接退出,不会继续做任何安装动作。
如果你只想在仓库目录里做一次手动更新,也保留了一个轻量部署脚本:
./scripts/deploy.sh
它会按顺序执行:
- 检查
.env - 准备数据目录
- 如果当前目录是 git 仓库,执行
git pull --ff-only - 执行
docker compose pull web - 执行
docker compose up -d - 输出容器状态
- 输出最近日志
这个脚本的目标不是做平台化发布,而是让“更新代码并刷新容器”变成一个稳定、可重复执行的动作。
如果你不想自动拉代码,也可以直接手动运行:
docker compose pull
docker compose up -d
数据持久化
当前 SQLite 文件默认会保存在:
- 宿主机:
./data/app.db - 容器内:
/app/data/app.db
这是因为 docker-compose.yml 把:
${DATA_DIR:-./data}
挂载到了容器内:
/app/data
因此:
- 容器重建不会删除宿主机上的数据库文件
- 更新镜像不会导致 SQLite 数据丢失
- 只要保留
DATA_DIR目录,数据就还在
备份与恢复
备份机制
安装脚本会把渲染后的备份脚本安装到:
APP_DIR/backup_db.sh
并为当前用户创建一条 cron:
10 2 * * *
备份行为如下:
- 目标目录是
BACKUP_DIR - 备份文件名带时间戳,例如
app-20260421-021000.db - 最多保留 5 个本地备份
- 如果
BACKUP_REMOTE非空,会在本地备份完成后调用rclone copyto
SQLite 一致性策略:
- 备份脚本优先使用
sqlite3的.backup - 不停容器
- 不直接
cp正在写入的数据库文件
这样可以在应用仍然运行时生成事务一致的快照,避免简单文件复制带来的损坏风险。
手动执行备份
如果你想手动触发一次备份:
sh "$APP_DIR/backup_db.sh"
如果当前还没有执行安装脚本,也可以在仓库内手动准备 .env 后运行:
./scripts/backup_db.sh
前提是先通过安装脚本把它渲染并部署到 APP_DIR,因为仓库内版本本身是带占位符的模板。
恢复大致步骤
停止容器后,把备份文件覆盖回去:
cd "$APP_DIR"
docker compose stop
cp "$BACKUP_DIR/app-YYYYMMDD-HHMMSS.db" "${DATA_DIR:-./data}/app.db"
docker compose up -d
如果 DATA_DIR 是相对路径,记得在 APP_DIR 下执行这些命令。
nginx 与证书约定
仓库提供的 nginx 模板位于:
scripts/nginx/moving-helper.nginx.template
安装脚本会把它渲染成 Debian / Ubuntu 风格的站点配置:
/etc/nginx/sites-available/moving-helper-nginx/etc/nginx/sites-enabled/moving-helper-nginx
模板约定:
- 80 端口强制跳转到 443
- 443 默认启用 SSL
- 反代到仅绑定在本机回环地址上的
127.0.0.1:${APP_PORT} client_max_body_size 0
证书文件需要由用户自己准备在 SSL_PATH 下,当前模板默认引用:
fullchain.pemprivkey.key
常见排查
1. 查看容器日志
docker compose logs -f web
2. 确认服务是否已启动
docker compose ps
如果状态是 Up,通常说明容器已经跑起来了。
3. 确认端口映射
docker compose port web 10000
4. 确认数据库文件还在
ls -lh data
如果看到 app.db,说明宿主机持久化文件还在。
5. 容器更新后数据为什么没丢
因为数据库不放在镜像里,而是放在宿主机挂载目录 DATA_DIR 中。
镜像更新只会替换应用代码和运行环境,不会覆盖这个宿主机目录。
测试
运行测试:
python -m pytest
测试使用独立测试数据库,不会污染真实开发数据。
当前测试覆盖包括:
- Box / Item / SubItem 基础 CRUD
- 图片上传、替换、删除与错误路径
- 全局搜索 name / note
- 创建后的重定向行为
- 关键页面结构和 UX 文案
CI / CD
仓库现在包含两条基础自动化流程,文件位于:
.github/workflows/test.yml.github/workflows/docker-image.yml
CI:branch push 自动跑 pytest
test.yml 会在任意 branch 的 push 上执行:
- checkout 代码
- 使用 Python
3.12 - 安装
requirements.txt - 运行
pytest
当前测试不依赖外部数据库服务。
测试使用 tmp_path 创建独立 SQLite 文件,并通过 configure_database(...) 切换到临时数据库,因此:
- 不会污染
./data/app.db - 不要求额外启动 Docker 或 Compose
- 不要求额外配置测试环境变量
CD:tag 发布 Docker image
docker-image.yml 会在推送符合 v* 格式的 tag 时触发,例如:
v1.0.0v1.2.3
workflow 会先校验该 tag 指向的提交是否可从 origin/main 到达;只有满足这个条件的 tag 才会继续构建并推送镜像。
镜像发布目标:
- Registry Host:
code.wanderingbadger.dev - Image Name:
${{ github.repository }} - Platforms:
linux/amd64linux/arm64
推送的 tag 策略:
${tag}latest
例如仓库名为 tliu93/2026-moving-helper,打出 v1.0.0 后会推送:
code.wanderingbadger.dev/tliu93/2026-moving-helper:v1.0.0
code.wanderingbadger.dev/tliu93/2026-moving-helper:latest
Actions / Gitea Secrets
需要在仓库的 Actions secrets 中配置:
REGISTRY_USERNAMEREGISTRY_TOKEN
推荐含义:
REGISTRY_USERNAME: Gitea 用户名REGISTRY_TOKEN: 具备 Container Registry 推送权限的 Access Token
如果你的 Gitea 实例对 package / registry 权限做了单独控制,确保这个 token 至少具备对应仓库的镜像推送权限。
如何触发镜像发布
建议流程:
git checkout main
git pull --ff-only origin main
git tag v1.0.0
git push origin main --tags
如果只想推送单个 tag:
git push origin v1.0.0
本地手动构建镜像
单架构本地构建:
docker build -t moving-helper:local .
本地运行:
docker run --rm -p 10000:10000 \
-e DATABASE_URL=sqlite:////app/data/app.db \
moving-helper:local
如果要模拟发布时的多架构构建,可以使用 buildx:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t code.wanderingbadger.dev/${USER}/2026-moving-helper:test \
--load \
.
一次性 Notion 导入
项目内附带了一个一次性迁移脚本:
python scripts/import_notion.py --dry-run
python scripts/import_notion.py --apply
说明:
- 这是一次性 migration / import 工具,不是长期同步功能
- 运行时会交互要求输入:
- Notion API token
- Notion 页面完整 URL
--dry-run只读取和解析,不写数据库--apply会真正写入当前 SQLite 数据库- 建议导入前先备份
data/app.db
当前支持的 Notion 结构映射
heading_2->Box- 某个
heading_2下的一级 bullet ->Item - 如果一级 bullet 下还有二级 bullet:
- 一级 bullet -> 容器型
Item - 二级 bullet ->
SubItem
- 一级 bullet -> 容器型
当前最大只处理到这个层级:
heading_2
└── 一级 bullet
└── 二级 bullet
更深层级会在日志中提示,但不会继续扩展成无限树。
这一版不导入图片
这一版导入脚本:
- 不下载图片
- 不导入图片
- 遇到图片或其他媒体 block 时会提示已跳过
图片后续可以在应用里手动补录。