Fork Recipes 日常运维与升级手册
Fork Recipes 双通道备份恢复(backupper 快照 201/204/201 与 pg_dump 49KB 异库校验闭环)、四容器巡检清单、docker update 垂直调整、pull+up 升级与 pinned tag 回滚锚点、migrate 幂等实测,全链路运维手册附真实回显。
Fork Recipes 日常运维与升级手册
目标:给已按本专栏 《Fork Recipes Docker Compose 部署指南》 完成部署(nginx + forkapi + fork.recipes + PostgreSQL 四容器形态)的运维者一套可照抄的巡检、备份恢复、监控、升级回滚流程。本文全部命令与回显为同一台 Ubuntu 24.04 宿主机(IP
192.168.1.172,端口 8146/8148/5434)2026-11-01 实测。项目架构背景见 《Fork Recipes 介绍与选型指南》。
一、环境信息
被管对象(docker compose -f docker-compose-postgres.yml ps 实测稳态):
fork-recipes-db db running
forkapi be running
nginx nginx running
recipes web running
- 版本口径:前端 fork.recipes
v.4.1.1-Hot-fix(镜像forkrecipes:latest,本地构建)、后端镜像mikebgrep/forkapi:latest(v.6.1.1.Hot-fix 系列)、postgres:17-alpine、nginx:fork-recipes - 数据落点:业务主数据在 PostgreSQL(bind 宿主
tmp/fork-recipes/data/postgres)、食谱图片/视频/快照在命名卷forkrecipes_web_media、前端会话 SQLite 在容器层(官方挂载错位,不持久化——影响与修法见 6.4 与常见问题) - 工作目录约定:
cd /opt/data/quzuo/tmp/fork-recipes/deploy/fork.recipes(compose 文件所在目录),备份产物落tmp/fork-recipes/backup/
二、日常巡检
2.1 容器状态与重启次数
$ docker ps --format '{{.Names}}\t{{.Status}}' | grep -E 'nginx|forkapi|recipes|fork-recipes-db'
nginx Up 22 minutes
recipes Up 9 minutes
forkapi Up 22 minutes
fork-recipes-db Up 22 minutes
要点:recipes 的 Up 时间明显短于其他三个是正常的(本文 5.3 做过一次 restart web 演练);异常信号是反复出现 Up Less than a minute(崩溃循环),立刻 docker logs <容器> 看尾部。
2.2 uwsgi socket 就绪(nginx↔前后端的命脉)
nginx 靠共享卷里两个 socket 文件转发,缺一即 502:
$ docker exec nginx ls -la /tmp/uwsgi/
total 8
drwxr-xr-x 2 root root 4096 Nov 1 14:57 .
drwxrwxrwt 1 root root 4096 Nov 1 14:44 ..
srw-rw---- 1 www-data www-data 0 Nov 1 14:44 uwsgi.sock
srw-rw---- 1 www-data www-data 0 Nov 1 14:57 uwsgi_recipes.sock
uwsgi.sock=后端 API,uwsgi_recipes.sock=前端 UI;文件 mtime 即对应进程最近一次拉起时间。
2.3 服务可用性三点探测
$ curl -s -o /dev/null -w "FE login page HTTP:%{http_code}\n" http://192.168.1.172:8146/login/
FE login page HTTP:200
$ curl -s -o /dev/null -w "API category HTTP:%{http_code}\n" http://192.168.1.172:8148/api/recipe/category -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2"
API category HTTP:200
$ curl -s -o /dev/null -w "admin panel HTTP:%{http_code} -> %{redirect_url}\n" http://192.168.1.172:8148/admin/
admin panel HTTP:302 -> http://192.168.1.172:8148/login/?next=/admin/
判定:前端 200、只读 API 200(X-Auth-Header 值取自 .env)、管理面板 302 到登录页,三者齐即全链路健康;任何一环 502 先查 2.2 的 socket,500 先查 docker logs forkapi。
2.4 资源占用基线
$ docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
nginx 0.00% 7.406MiB / 15.57GiB
recipes 0.00% 35.75MiB / 15.57GiB
forkapi 0.00% 184.8MiB / 15.57GiB
fork-recipes-db 0.00% 78.54MiB / 15.57GiB
空闲基线合计约 306 MiB;forkapi 是内存大头(uwsgi 多 worker + 抓取任务),本文第五章已把它限额到 512 MiB。
2.5 磁盘水位
$ du -sh /opt/data/quzuo/tmp/fork-recipes/data/postgres
64M /opt/data/quzuo/tmp/fork-recipes/data/postgres
增长监控对象:PG 数据目录(结构化数据)、forkrecipes_web_media 卷(图片/视频/快照 zip,视频食谱会显著放大)、镜像本身 2.96 GB 为固定成本。
三、备份与恢复
Fork Recipes 有两条互补备份通道,都要用起来:
| 通道 | 覆盖范围 | 不覆盖 | 适用 |
|---|---|---|---|
应用层 api/backupper 快照 | 业务模型全量(data.json:食谱/食材/步骤/分类/用户…)+ 食谱媒体文件 | 前端 SQLite、PG 系统表、uwsgi 配置 | 迁移到新实例、误删食谱回滚 |
数据库层 pg_dump | PostgreSQL 全部表与序列 | 媒体文件(图片在卷里) | 日常定时备份、库级恢复 |
3.1 backupper 快照:创建
$ curl -s -X POST http://192.168.1.172:8148/api/backupper/ \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" -w "\nHTTP:%{http_code}\n"
["Successfully created backup file: backups/fork_recipes_2026.11.01_14.50.35.zip"]
HTTP:201
快照 zip 直接落在 web_media 卷的 backups/ 子目录,可经 media URL 下载:
$ curl -s -o bk.zip -w "HTTP:%{http_code} size:%{size_download}\n" \
http://192.168.1.172:8148/media/backups/fork_recipes_2026.11.01_14.50.35.zip
HTTP:200 size:2580
$ docker exec forkapi python -c "import zipfile;print(zipfile.ZipFile('media/backups/fork_recipes_2026.11.01_14.50.35.zip').namelist())"
['data.json', 'images/b3a050cf-911d-46de-b8fc-6faa8badc9ea_tomato.png']
(zip 内容 = data.json + 食谱图片,实测吻合。)
3.2 backupper 快照:列表、恢复、导入
$ curl -s http://192.168.1.172:8148/api/backupper/ \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2"
[{"pk":1,"file":"http://192.168.1.172:8148/media/backups/fork_recipes_2026.11.01_14.50.35.zip","created_at":"2026-11-01T14:50:35.228000Z","size":2580},{"pk":2,"file":"http://192.168.1.172:8148/media/backups/fork_recipes_imported.zip","created_at":"2026-11-01T15:01:28.592505Z","size":2580}]
恢复到当前实例(覆盖式:解包重放 data.json,先建快照再操作):
$ curl -s -X PATCH http://192.168.1.172:8148/api/backupper/ \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-H "Content-Type: application/json" -d '{"backup_pk":1}' -w "HTTP:%{http_code}\n"
HTTP:204
从外部 zip 导入(跨实例迁移入口,只认 .zip 后缀):
$ curl -s -X POST http://192.168.1.172:8148/api/backupper/import/ \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-F "file=@bk.zip;filename=fork_recipes_imported.zip" -w "\nHTTP:%{http_code}\n"
"fork_recipes_imported.zip"
HTTP:201
导入后它出现在列表(pk=2),再 PATCH {"backup_pk":2} 即完成「新实例装空库 → 导入 zip → 应用快照」的迁移闭环。
3.3 pg_dump 数据库备份(含异库恢复校验)
备份:
$ docker exec fork-recipes-db pg_dump -U forkuser -d forkrecipes > backup/pg_dump_2026.11.01.sql
$ ls -la backup/pg_dump_2026.11.01.sql
-rw-r--r-- 1 root root 49533 Nov 1 23:01 backup/pg_dump_2026.11.01.sql
$ grep -c "CREATE TABLE\|INSERT INTO" backup/pg_dump_2026.11.01.sql
23
恢复演练(恢复到同实例新建的临时库做完整性校验,不碰生产库):
$ docker exec -i fork-recipes-db psql -U forkuser -d postgres -c "CREATE DATABASE forkrecipes_rt OWNER forkuser"
CREATE DATABASE
$ docker exec -i fork-recipes-db psql -U forkuser -d forkrecipes_rt -q < backup/pg_dump_2026.11.01.sql
$ echo RESTORE_OK
RESTORE_OK
$ docker exec fork-recipes-db psql -U forkuser -d forkrecipes_rt -c "select count(*) from recipe_recipe;" -c "select count(*) from recipe_ingredient;" -c "select count(*) from recipe_category;"
count
-------
1
(1 row)
count
-------
2
(1 row)
count
-------
4
(1 row)
行数与生产库(1 食谱 / 2 食材 / 4 分类)完全一致,恢复有效;校验完删除临时库:
$ docker exec fork-recipes-db psql -U forkuser -d postgres -c "DROP DATABASE forkrecipes_rt;"
DROP DATABASE
真要就地恢复生产库时(灾难场景):docker compose stop be web nginx → psql -U forkuser -d postgres -c "DROP DATABASE forkrecipes;" → CREATE DATABASE forkrecipes OWNER forkuser → 灌入 dump → docker compose up -d。
3.4 媒体卷备份
图片/视频/快照 zip 都在 forkrecipes_web_media 卷,用一次性容器打包(只读挂载,不影响服务):
$ docker run --rm -v forkrecipes_web_media:/mnt:ro -v $(pwd):/backup alpine \
tar czf /backup/web_media_2026.11.01.tar.gz -C /mnt .
$ ls -la web_media_2026.11.01.tar.gz
-rw-r--r-- 1 root root 3195 Nov 1 23:06 web_media_2026.11.01.tar.gz
$ tar tzf web_media_2026.11.01.tar.gz | head -6
./
./backups/
./backups/fork_recipes_imported.zip
./backups/fork_recipes_2026.11.01_14.50.35.zip
./backups/readme.md
./videos/
恢复对偶命令:docker run --rm -v forkrecipes_web_media:/mnt -v $(pwd):/backup alpine tar xzf /backup/web_media_2026.11.01.tar.gz -C /mnt。
3.5 建议备份计划(cron 示例)
0 3 * * * docker exec fork-recipes-db pg_dump -U forkuser -d forkrecipes > /opt/data/quzuo/tmp/fork-recipes/backup/pg_$(date +\%F).sql
30 3 * * * docker run --rm -v forkrecipes_web_media:/mnt:ro -v /opt/data/quzuo/tmp/fork-recipes/backup:/backup alpine tar czf /backup/media_$(date +\%F).tar.gz -C /mnt .
💡 技术拓展:backupper 的
data.json本质是 Django 模型序列化全量导出(含用户与密码 hash),与 pg_dump 的差别在于跨数据库形态可移植(SQLite 实例的快照可以导入 PostgreSQL 实例);pg_dump 则是库级精确快照、可异库校验。两者互为校验,别只留一份。
四、监控与告警
应用无内建指标端点(源码无 Prometheus/metrics 模块,与 Fider 不同),监控靠日志 + 容器指标三层:
- 入口层 nginx access log(
docker logs nginx直出,格式含状态码):
$ docker logs nginx --tail 3
192.168.1.172 - - [01/Nov/2026:14:57:19 +0000] "GET /api/recipe/home/preview/?page=1 HTTP/1.1" 200 268 "-" "curl/8.5.0"
192.168.1.172 - - [01/Nov/2026:15:01:28 +0000] "POST /api/backupper/import/ HTTP/1.1" 201 27 "-" "curl/8.5.0"
192.168.1.172 - - [01/Nov/2026:15:01:28 +0000] "GET /api/backupper/ HTTP/1.1" 200 290 "-" "curl/8.5.0"
异常码速查:502=对应 uwsgi socket 未就绪(查 2.2);403=API 忘带 X-Auth-Header;500=应用异常(查第 2 层日志)。
- 应用层:
docker logs forkapi(uwsgi 请求行 + 迁移/任务输出)、docker logs recipes(前端)。请求行样例(实测):
[pid: 8|app: 0|req: 5/5] 192.168.1.172 () {38 vars in 545 bytes} [Sun Nov 1 14:46:41 2026] POST /api/auth/token => generated 128 bytes in 147 msecs (HTTP/1.1 200) 8 headers in 243 bytes
- 资源层:
docker stats --no-stream(基线见 2.4)+du -sh data/postgres(2.5)。
最小外部探活脚本(可接进现有监控):
#!/bin/sh
for u in "http://192.168.1.172:8146/login/" "http://192.168.1.172:8148/api/recipe/category"; do
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" "$u")
[ "$code" = "200" ] || echo "ALERT $u -> $code"
done
实测两个探测点分别返回 200/200,脚本无 ALERT 输出即健康。
五、扩容缩容
5.1 水平扩展不可行的原因(架构约束)
官方 compose 的 be/web 用 network_mode: host + 固定容器名 + unix socket 共享卷 + 前端会话存本地 SQLite:同机起第二个 forkapi 会撞容器名与 host 网络端口,nginx 也只认卷里那一个 socket 文件。结论:Fork Recipes 按单实例规划,多实例并行需要自行改造(去 host 网络、socket 改 TCP uwsgi_pass、会话集中化),官方不支持。
5.2 垂直调整(实测)
给后端限内存(防抓取任务 OOM 拖垮整机):
$ docker update --memory 512m --memory-swap 512m forkapi
forkapi
$ docker inspect forkapi --format '{{.HostConfig.Memory}}'
536870912
要点:docker update 即时生效不用重启容器;但该限额不写回 compose,容器重建即失效——长期生效请写进 compose 的 mem_limit: 字段。Docker 29 实测 docker update --memory 0 不能复位为不限(仍显示 536870912),复位需 down + up -d 重建。
5.3 单服务重启(配置改动后)
$ docker compose -f docker-compose-postgres.yml restart web
Container recipes Restarting
Container recipes Started
前端 .env 改动(如 SERVICE_BASE_URL)对 web/be 重启即生效;nginx conf 改动必须重建镜像(配置 COPY 进镜像,见部署篇第七章)。
六、升级与回滚
6.1 升级前置:双通道备份
先做 3.1(backupper 快照)+ 3.3(pg_dump),再动版本。
6.2 后端升级(镜像级)
$ docker compose -f docker-compose-postgres.yml pull be
Image mikebgrep/forkapi:latest Pulling
Image mikebgrep/forkapi:latest Pulled
$ docker compose -f docker-compose-postgres.yml up -d
Container recipes Running
Container nginx Running
Container forkapi Running
Container fork-recipes-db Running
要点:latest 标签无变化时 up -d 显示 Running(不重建,实测);拉到新摘要后 compose 会自动重建 be 容器,启动命令里的 migration.sh 自动跑迁移——重复执行是幂等的(实测):
$ docker exec forkapi python manage.py migrate
Running migrations:
No migrations to apply.
生产建议用摘要或版本 tag 钉死镜像(image: mikebgrep/forkapi:<tag>),不要常年跟 latest:项目发版集中在 2025 上半年,latest 与 tag 内容漂移不可控。
6.3 前端升级(源码构建级)
前端无官方镜像,升级=切 tag + 重建:
git fetch --tags && git checkout v.4.1.1-Hot-fix # 目标 tag
vi Dockerfile # 确认 FROM 仍是 python:3.12-slim-bookworm(trixie 坑,见排错篇)
docker compose -f docker-compose-postgres.yml build web
docker compose -f docker-compose-postgres.yml up -d web
6.4 回滚
镜像回滚锚点(升级前先打,实测):
$ docker tag mikebgrep/forkapi:latest mikebgrep/forkapi:pinned-20261101
$ docker images --format '{{.Repository}}:{{.Tag}}\t{{.ID}}' | grep 'mikebgrep/forkapi'
docker.1ms.run/mikebgrep/forkapi:latest 68ba6c32870d
mikebgrep/forkapi:latest 68ba6c32870d
mikebgrep/forkapi:pinned-20261101 68ba6c32870d
hub.rat.dev/mikebgrep/forkapi:latest 68ba6c32870d
回滚=把 compose 的 be 镜像指回 pinned 标签后 up -d(三个标签同一镜像 ID 68ba6c32870d,指回即还原)。
数据库回滚的硬约束:Django 迁移没有自动 down——新镜像一旦跑过迁移,旧镜像可能读不懂新 schema。回滚顺序必须是:改回旧镜像 → 用 3.3 的 pg_dump 恢复升级前库 → 起服务。若只是应用行为异常而 schema 未变,直接回镜像即可。
应用数据回退(误删食谱等):不碰镜像,直接 PATCH api/backupper/ 应用升级前快照(3.2 实测 204)。
6.5 版本口径提醒
本文部署的前端 tag v.4.1.1-Hot-fix 钉 Django 5.1.8,而 fork.recipes master 分支已升 Django 5.2.x——切 master 升级属于跨大版本行为,先备份、在测试实例验证登录与建谱后再动生产。
七、故障排查手册(定位流程)
现象 → 第一步看哪里
502(前端或 API 全挂) → docker exec nginx ls /tmp/uwsgi/(2.2)→ 缺哪个 socket 就 docker logs 对应容器
500(API 报错) → docker logs forkapi 尾部 50 行;先排除 GET /api/recipe/ 根路径(官方必 500,用 home/preview)
登录成功又跳回登录页 → .env 的 SERVICE_BASE_URL 是否宿主机可达;DJANGO_SECRET 前后端是否同值
图片不显示(URL 404) → web_media 卷是否挂全(be+nginx 两处)、nginx 81 端口 /media 块是否在
建谱 400 → servings/prep_time/cook_time 必须整数、image 必填(部署篇 4.7-④)
重建容器后要重新登录 → 前端 SQLite 挂载错位(设计内行为,数据无损)
compose up 报端口占用 → ss -ltn 找占用方,改 compose ports 映射(本项目 8146/8148/5434 为避让产物)
逐条现象→根因→解法的完整实录见本专栏《Fork Recipes 故障排查与 FAQ》。
八、常见问题
Q1:backupper 快照里有用户密码吗?
有。data.json 含用户模型全量(密码为 hash),快照 zip 按敏感文件管理,media URL 是匿名可下载的(实测 /media/backups/...zip 200 无需认证)——公网部署务必给 8148 加反代认证或防火墙。
Q2:SEED_DEFAULT_DATA=true 会反复覆盖数据吗?
不会。seed 只在首次 migrate 时建管理员与分类(get_or_create 语义),第二次启动 No migrations to apply. 后 seed 也不重复插(实测升级演练后分类仍 4 个、食谱仍 1 个)。
Q3:改了管理员密码忘了怎么办? 命令行重置(不依赖邮件找回,实测回显):
$ printf 'zz123456\nzz123456\n' | docker exec -i forkapi python manage.py changepassword admin
Password changed successfully for user 'admin'
非交互场景(如批量脚本)改用 shell 直设,实测:
$ docker exec forkapi python manage.py shell -c "from django.contrib.auth import get_user_model; u=get_user_model().objects.get(username='admin'); u.set_password('ChangeMe'); u.save(); print('password reset ok')"
password reset ok
实测把 admin 改成 zz123456 后,再用 shell 方式回改为种子密码 ChangeMe,POST api/auth/token 验证登录恢复且返回原 token。也可在管理面板用户列表直接改。
Q4:备份要备几份? 最少三件套:pg_dump(结构化)+ web_media tar(媒体)+ backupper zip(跨形态迁移用)。前端 SQLite 无需备份(会话数据,丢了重新登录)。
九、参考链接
- 前端仓库:https://github.com/mikebgrep/fork.recipes
- 后端仓库:https://github.com/mikebgrep/forkapi
- 官方文档站:https://mikebgrep.github.io/forkapi/latest/
- 本专栏:《Fork Recipes 介绍与选型指南》《Fork Recipes Docker Compose 部署指南》《Fork Recipes 故障排查与 FAQ》