ManageMeals Docker Compose 部署指南
ManageMeals web v1.7.3/api v1.9.3 官方 docker-compose 八服务栈在 Ubuntu 24.04/Docker 29.9.2 实测部署:ghcr 仅 :latest 的 digest 版本固定、注册/登录/URL导入/搜索端到端验证、bind mount 持久化与 mongodump 备份恢复、环境变量表与运维命令、FAQ(实测日期 2026-11-02)
ManageMeals Docker Compose 部署指南
目标:在一台干净的 Ubuntu 24.04 服务器上,用官方 docker-compose 从零部署 ManageMeals(web v1.7.3 / api v1.9.3,2026-09-25 发布)全套 8 服务栈,完成注册、登录、URL 导入菜谱与全文搜索的端到端验证。全部命令与回显均来自实测环境(Ubuntu 24.04.5 LTS / x86_64 / Docker 29.9.2 / Docker Compose v5.6.1,实测日期 2026-11-02)。组件功能与选型评估见站内文章 《ManageMeals 介绍与选型指南》。
⚠️ 验收方式说明:本次实测环境无浏览器自动化工具可用,前端页面验收降级为「HTTP 状态码 + 页面
<title>+ 未登录 307 跳转行为 + 后端 API 实测」组合验证,未做像素级 UI 走查;功能链路(注册/登录/导入/搜索)均通过 API 实测闭环。
一、环境信息
本节列出本次部署实测所用的全部可复现环境信息,读者对照自己的服务器替换即可。
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 24.04.5 LTS(x86_64) |
| Docker 引擎 | 29.9.2 |
| Docker Compose | v5.6.1(docker compose 子命令形态) |
| 应用版本 | manage-meals-web v1.7.3、manage-meals-api v1.9.3(均 2026-09-25 发布;ghcr 仅发 :latest 标签,见「二、镜像选型说明」) |
| 依赖组件 | mongo:7、valkey/valkey:8、typesense/typesense:28.0、mongo-express |
| 部署目录 | /opt/manage-meals/(compose 文件 + data/ 持久化目录) |
| Compose 项目名 | manage-meals-v1.9.3-stack(compose 文件顶层 name: 字段) |
| 专用网络 | manage-meals-net,子网 10.89.63.0/24 |
| 对外端口 | web 8146、api 8148、typesense 8151、scraper 8153、mongo-express 8081;mongo/valkey 不映射宿主端口 |
| 演示账号 | 邮箱 admin@example.com,密码 q1w2e3r4(注册后写入 MongoDB users 集合) |
| 数据库凭据 | MongoDB root / q1w2e3r4;Typesense API key q1w2e3r4 |
| 实测资源占用 | 8 服务空闲态合计约 320 MiB 内存(见 3.3) |
二、镜像选型说明
ManageMeals 官方镜像全部发布在 GitHub Container Registry(ghcr.io/managemeals/),自托管需要 4 个官方应用镜像加 4 个依赖镜像,共 8 个镜像。本节先讲清「版本怎么选」,因为这是 ManageMeals 部署最容易踩的坑。
2.1 镜像清单
| 服务 | 镜像 | 实测大小 | 角色 |
|---|---|---|---|
| manage-meals-web | ghcr.io/managemeals/manage-meals-web:latest | 587MB | SvelteKit 前端(SSR,监听 3000) |
| manage-meals-api | ghcr.io/managemeals/manage-meals-api:latest | 479MB | Fastify 后端 API(监听 3000) |
| recipe-scraper | ghcr.io/managemeals/manage-meals-scraper:latest | 174MB | Python/Flask 菜谱 URL 抓取器(监听 8000) |
| search-sync | ghcr.io/managemeals/manage-meals-search-sync:latest | 236MB | MongoDB→Typesense 搜索索引同步器 |
| mongo | mongo:7 | 1.18GB | 主数据库(库名 manage_meals) |
| valkey | valkey/valkey:8 | 170MB | Redis 兼容缓存(API 会话/缓存) |
| typesense | typesense/typesense:28.0 | 1.19GB | 全文搜索引擎(监听 8108) |
| mongo-express | mongo-express | 287MB | MongoDB Web 管理界面(可选,调试用) |
2.2 官方应用镜像只有 :latest,没有版本 tag
实测直接按版本号拉取会失败:
docker pull ghcr.io/managemeals/manage-meals-api:v1.9.3
Error response from daemon: failed to resolve reference "ghcr.io/managemeals/manage-meals-api:v1.9.3": ghcr.io/managemeals/manage-meals-api:v1.9.3: not found
即 ghcr.io/managemeals/ 下 4 个镜像只发布 :latest 单一标签,GitHub Releases 上的版本号(api v1.9.3 / web v1.7.3)不对应镜像 tag。要锁定本次实测的 v1.9.3/v1.7.3 版本,只能拉 :latest 后用镜像摘要(digest)固定。实测环境部署后 4 个官方镜像的 digest 为:
docker image inspect ghcr.io/managemeals/manage-meals-api:latest --format '{{index .RepoDigests 0}}'
ghcr.io/managemeals/manage-meals-api@sha256:01757ea28191c0f92401aacee9c6050568e205cec7eb32646038ce965454de83
ghcr.io/managemeals/manage-meals-web@sha256:9886d09bbee8ea9da60a320757cb5eb15fa5740b701e5b5b86377d59b1ff0a56
ghcr.io/managemeals/manage-meals-scraper@sha256:19a97e0fa3da1c1ccc4e0baca243fde477f4a7fceaf22714755e34e143b74afa
ghcr.io/managemeals/manage-meals-search-sync@sha256:2011956445de238d41146a8fdd50b5eb01618935330ef41391555ece6319ff46
(后三行为对各自镜像执行同一命令的结果,逐条实测。)生产环境若要求可复现部署,把 compose 中的 image: 改写为 镜像名@sha256:... 形式即可。
💡 技术拓展:容器镜像的版本固定方式不止
:latest一种。常见策略对比——
策略 写法 优点 风险/代价 浮动标签 image: app:latest永远最新,官方唯一提供 不可复现,升级即换包 版本标签 image: app:v1.9.3可读、可复现 ManageMeals 官方未发布此类 tag(2.2 实测 not found) 摘要固定 image: app@sha256:...字节级可复现 不可读,需配合 docker image inspect记录对应版本私有仓库转存 同步到 Harbor/ECR 后自打 tag 内网分发、供应链管控 需要额外同步设施 本文主线采用「
:latest拉取 + digest 记录固定」,因为这是 ManageMeals 官方发布形态下唯一可复现的做法。
2.3 拉取渠道提示
实测环境经宿主 HTTP 代理直连 ghcr.io 拉取全部成功;curl -sI https://ghcr.io/v2/ 返回 401(registry 未认证端点的正常响应,说明网络可达)。国内服务器直连 ghcr 常超时,建议为 Docker 配置代理或走镜像加速,raw.githubusercontent.com(取 .env 样例、源码)同样存在间歇失败,重试即可。
三、前置条件
部署前确认三件事:Docker 组件版本、端口空闲、资源余量。
3.1 Docker 与 Compose 版本
docker compose version
docker version --format 'Client: {{.Client.Version}} / Server: {{.Server.Version}}'
Docker Compose version v5.6.1
Client: 29.9.2 / Server: 29.9.2
官方 Wiki 的 compose 样例使用现代 docker compose(v2 语法,无 version: 字段),Compose v2.x 以上均可。
3.2 端口占用检查
ss -tln | grep -E ':(8146|8148|8151|8153|8081)\b'
部署前该命令应无输出(端口空闲)。部署完成后同样的命令输出五个监听端口,即验证成功:
:8081
:8146
:8148
:8151
:8153
mongo(27017)与 valkey(6379)在本方案中不映射宿主端口,仅走 manage-meals-net 内部网络,无需检查。
3.3 资源要求
实测栈空闲态资源占用(docker stats --no-stream,宿主总内存 15.57GiB):
manage-meals-v193-stack-manage-meals-api-1 0.00% 71.2MiB / 15.57GiB
manage-meals-v193-stack-manage-meals-web-1 0.00% 19.82MiB / 15.57GiB
manage-meals-v193-stack-mongo-1 0.54% 79.99MiB / 15.57GiB
manage-meals-v193-stack-mongo-express-1 0.00% 39.5MiB / 15.57GiB
manage-meals-v193-stack-recipe-scraper-1 0.01% 50.79MiB / 15.57GiB
manage-meals-v193-stack-search-sync-1 0.16% 17.05MiB / 15.57GiB
manage-meals-v193-stack-typesense-1 0.08% 38.15MiB / 15.57GiB
manage-meals-v193-stack-valkey-1 0.08% 3.844MiB / 15.57GiB
8 服务合计约 320MiB 内存,磁盘镜像合计约 4.3GB,1 核 2GB 的小机型即可承载个人/家庭用量;mongo-express 属调试件,生产可去掉以省 40MiB。
四、部署步骤
本节从目录准备开始,一次拉起 8 服务栈并完成端到端功能验证,全部命令逐条实测。
4.1 准备目录与 compose 文件
创建部署目录与持久化子目录:
mkdir -p /opt/manage-meals/data/mongo /opt/manage-meals/data/typesense
cd /opt/manage-meals
docker-compose.yaml 全文如下(在官方 Wiki 自托管样例基础上,按本环境端口与凭据调整;顶层 name 固定项目名,networks 显式指定子网便于排查):
name: manage-meals-v1.9.3-stack
services:
manage-meals-api:
image: ghcr.io/managemeals/manage-meals-api:latest
restart: unless-stopped
environment:
MONGO_URL: mongodb://root:q1w2e3r4@mongo:27017/
MONGO_DB: manage_meals
REDIS_URL: redis://valkey:6379
TYPESENSE_API_KEY: q1w2e3r4
TYPESENSE_HOST: typesense
TYPESENSE_PORT: 8108
ACCESS_JWT_SECRET: access-jwt-secret-q1w2e3r4
REFRESH_JWT_SECRET: refresh-jwt-secret-q1w2e3r4
AUTH_ACCESS_TOKEN_EXPIRE_SEC: 600
AUTH_REFRESH_TOKEN_EXPIRE_SEC: 2629746
RECIPE_SCRAPER_URL: http://recipe-scraper:8000
USER_REGISTER_ENABLED: "true"
depends_on:
- mongo
- valkey
- recipe-scraper
- typesense
- search-sync
ports:
- 8148:3000
manage-meals-web:
image: ghcr.io/managemeals/manage-meals-web:latest
restart: unless-stopped
environment:
PUBLIC_MAIN_TITLE: ManageMeals
PASSWORD_MIN_LENGTH: 6
API_URL: http://manage-meals-api:3000/v1
COOKIE_ACCESS_TOKEN: mmeals_access_token
COOKIE_REFRESH_TOKEN: mmeals_refresh_token
COOKIE_ACCESS_TOKEN_EXPIRE_SEC: 600
COOKIE_REFRESH_TOKEN_EXPIRE_SEC: 2629746
ORIGIN: http://localhost:8146
BODY_SIZE_LIMIT: Infinity
depends_on:
- manage-meals-api
ports:
- 8146:3000
mongo:
image: mongo:7
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: q1w2e3r4
MONGO_INITDB_DATABASE: manage_meals
volumes:
- ./data/mongo:/data/db
mongo-express:
image: mongo-express
restart: unless-stopped
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: q1w2e3r4
ME_CONFIG_MONGODB_URL: mongodb://root:q1w2e3r4@mongo:27017/
ME_CONFIG_BASICAUTH: "false"
ME_CONFIG_BASICAUTH_USERNAME: ""
ME_CONFIG_BASICAUTH_PASSWORD: ""
depends_on:
- mongo
ports:
- 8081:8081
valkey:
image: valkey/valkey:8
restart: unless-stopped
recipe-scraper:
image: ghcr.io/managemeals/manage-meals-scraper:latest
restart: unless-stopped
ports:
- 8153:8000
typesense:
image: typesense/typesense:28.0
restart: unless-stopped
command: "--data-dir /data --api-key=q1w2e3r4 --enable-cors"
volumes:
- ./data/typesense:/data
ports:
- 8151:8108
search-sync:
image: ghcr.io/managemeals/manage-meals-search-sync:latest
restart: unless-stopped
environment:
MONGO_URL: mongodb://root:q1w2e3r4@mongo:27017/
MONGO_DB: manage_meals
TYPESENSE_API_KEY: q1w2e3r4
TYPESENSE_HOST: typesense
TYPESENSE_PORT: 8108
depends_on:
- mongo
- typesense
networks:
default:
name: manage-meals-net
ipam:
config:
- subnet: 10.89.63.0/24
逐服务要点:manage-meals-api 是业务核心,数据源、JWT、搜索、抓取器四类配置全部集中在它的 environment;manage-meals-web 只需知道 API 地址(API_URL)与自身对外地址(ORIGIN);mongo/valkey/typesense 为无状态依赖(数据靠挂载持久化);search-sync 每分钟轮询由内置 cron 控制(见 7.5);recipe-scraper 零配置可跑(可选参数见 7.6);mongo-express 仅供调试。
写完后先做语法校验(无输出即通过):
docker compose config --quiet && echo 'config ok (exit 0)'
config ok (exit 0)
4.2 启动栈
docker compose up -d
实测回显(节选,先建网络再按依赖顺序创建/启动 8 个服务):
Network manage-meals-net Creating
Network manage-meals-net Created
Container manage-meals-v193-stack-valkey-1 Creating
Container manage-meals-v193-stack-mongo-1 Creating
Container manage-meals-v193-stack-typesense-1 Creating
Container manage-meals-v193-stack-recipe-scraper-1 Creating
...
Container manage-meals-v193-stack-manage-meals-api-1 Starting
Container manage-meals-v193-stack-manage-meals-api-1 Started
Container manage-meals-v193-stack-manage-meals-web-1 Starting
Container manage-meals-v193-stack-manage-meals-web-1 Started
restart: unless-stopped 保证宿主重启后 Docker 自动拉起全栈。
4.3 确认服务状态
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
manage-meals-v193-stack-manage-meals-api-1 ghcr.io/managemeals/manage-meals-api:latest "docker-entrypoint.s…" manage-meals-api 28 seconds ago Up 26 seconds 0.0.0.0:8148->3000/tcp
manage-meals-v193-stack-manage-meals-web-1 ghcr.io/managemeals/manage-meals-web:latest "docker-entrypoint.s…" manage-meals-web 27 seconds ago Up 26 seconds 0.0.0.0:8146->3000/tcp
manage-meals-v193-stack-mongo-1 mongo:7 "docker-entrypoint.s…" mongo 28 seconds ago Up 26 seconds 27017/tcp
manage-meals-v193-stack-mongo-express-1 mongo-express "/sbin/tini -- /dock…" mongo-express 28 seconds ago Up 26 seconds 0.0.0.0:8081->8081/tcp
manage-meals-v193-stack-recipe-scraper-1 ghcr.io/managemeals/manage-meals-scraper:latest "gunicorn -w 1 -b 0.…" recipe-scraper 28 seconds ago Up 27 seconds 0.0.0.0:8153->8000/tcp
manage-meals-v193-stack-search-sync-1 ghcr.io/managemeals/manage-meals-search-sync:latest "docker-entrypoint.s…" search-sync 28 seconds ago Up 26 seconds
manage-meals-v193-stack-typesense-1 typesense/typesense:28.0 "/opt/typesense-serv…" typesense 28 seconds ago Up 26 seconds 0.0.0.0:8151->8108/tcp
manage-meals-v193-stack-valkey-1 valkey/valkey:8 "tini -- docker-entr…" valkey 28 seconds ago Up 27 seconds 6379/tcp
8 服务全部 Up。关键进程日志抽查:
docker compose logs manage-meals-web --tail 1
docker compose logs search-sync --tail 2
manage-meals-web-1 | Listening on http://0.0.0.0:3000
search-sync-1 | Starting search sync
search-sync-1 | Finished search sync
4.4 前端页面验证
本次环境无浏览器工具,按引言说明采用 HTTP 状态码 + <title> + 跳转行为验收:
curl -s -o /dev/null -w 'http=%{http_code}\n' http://localhost:8146/
curl -s http://localhost:8146/ | grep -o '<title>[^<]*</title>'
curl -s http://localhost:8146/auth/login | grep -o '<title>[^<]*</title>'
curl -s -o /dev/null -w 'http=%{http_code} redirect=%{redirect_url}\n' \
http://localhost:8146/recipes/best-ever-chocolate-brownies-recipe-1K978f4R
http=200
<title>ManageMeals - Recipe Manager</title>
<title>Login - ManageMeals</title>
http=307 redirect=http://localhost:8146/auth/login?goto=/recipes/best-ever-chocolate-brownies-recipe-1K978f4R
首页 200、登录页 200、未登录访问菜谱详情正确 307 跳转登录页并携带 goto 回跳参数,前端 SSR 与路由守卫均正常。浏览器访问 http://<服务器IP>:8146/ 即可进入界面。
4.5 后端与依赖服务验证
curl -s http://localhost:8148/v1/tags
curl -s http://localhost:8151/health
curl -s -H "x-typesense-api-key: q1w2e3r4" http://localhost:8151/collections | head -c 120
docker compose exec -T valkey valkey-cli ping
docker compose exec -T mongo mongosh -u root -p q1w2e3r4 --authenticationDatabase admin \
--quiet --eval 'db.getSiblingDB("manage_meals").getCollectionNames().join(",")'
curl -s -o /dev/null -w 'mongo-express http=%{http_code}\n' http://localhost:8081/
{"statusCode":401,"error":"Unauthorized","message":"Missing Authorization header"}
{"ok":true}
[{"created_at":1793557800,"default_sorting_field":"rating","enable_nested_fields":false,"fields":[{"facet":false,"index":true,
PONG
tags,syncs,shoppinglists,mealplans,users,cookbooks,recipes
mongo-express http=200
解读:API 对未带令牌的请求返回 401(认证中间件生效);Typesense 健康且 recipes 集合已由 search-sync 自动创建(default_sorting_field: rating);Valkey 应答 PONG;MongoDB 已建 7 个业务集合;mongo-express 管理台可访问。
4.6 注册第一个账号
前提:USER_REGISTER_ENABLED: "true"(4.1 已配置)。通过 API 注册演示账号:
curl -s -X POST http://localhost:8148/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"name":"admin","email":"admin@example.com","password":"q1w2e3r4"}'
{"uuid":"d575f1c9-2142-4451-ad8a-c4f09f285ca5"}
返回新用户 uuid 即注册成功;浏览器端等价操作是在 /auth/login 页切到 Register 标签填写。密码受 PASSWORD_MIN_LENGTH=6 约束。
4.7 登录并做功能冒烟
curl -s -X POST http://localhost:8148/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"q1w2e3r4"}'
{"accessToken":"eyJhbGciOiJIUzI1NiIs...","refreshToken":"eyJhbGciOiJIUzI1NiIs..."}
(回显中两段 JWT 已截断展示;accessToken 有效期 600 秒,见 7.1。)带令牌查询菜谱与导入第一条菜谱:
TOKEN=<上面返回的 accessToken>
curl -s -X POST "http://localhost:8148/v1/recipes/import?url=https://www.bbcgoodfood.com/recipes/best-ever-chocolate-brownies-recipe" \
-H "Authorization: Bearer $TOKEN"
{"slug":"best-ever-chocolate-brownies-recipe-1K978f4R"}
curl -s http://localhost:8148/v1/recipes -H "Authorization: Bearer $TOKEN" | head -c 200
{"page":1,"total":1,"perPage":20,"data":[{"uuid":"1da16971-edfb-40d7-b273-61bef9a4c6ae","slug":"best-ever-chocolate-brownies-recipe-1K978f4R","createdByUuid":"d575f1c9-2142-4451-ad8a-c4f09f285ca5",...
⚠️ 注意:
/v1/recipes/import的url是 querystring 参数而非 JSON body。实测把{"url":"..."}放 body 提交会返回{"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"querystring must have required property 'url'"}。
搜索验证:search-sync 按 cron 每 3 分钟增量同步索引(7.5),导入后立即搜索会 found:0,等待约 3 分钟后:
curl -s "http://localhost:8148/v1/search?q=brownies&c=recipes&p=1" -H "Authorization: Bearer $TOKEN" | head -c 200
{"facet_counts":[{"counts":[],"field_name":"categories",...}],"found":1,"hits":[{"document":{"categories":[],"createdByUuid":"d575f1c9-2142-4451-ad8a-c4f09f285ca5","description":"A super easy brownie recipe...
found:1 命中刚导入的《Best ever chocolate brownies recipe》,全文搜索链路打通。至此部署端到端验收完成。
五、数据持久化说明
本方案对两个有状态服务采用 bind mount 持久化,MongoDB 数据目录与 Typesense 索引目录都落在 compose 同级的 data/ 下,便于整目录备份与迁移。
5.1 挂载点与实测目录内容
mongo:
volumes:
- ./data/mongo:/data/db
typesense:
volumes:
- ./data/typesense:/data
运行一段时间后宿主侧实测:
ls data/
du -sh data/mongo data/typesense
ls data/mongo | head -3
mongo typesense
302M data/mongo
13M data/typesense
collection-0--2547051958837116079.wt
collection-0--9092349457587983004.wt
collection-10--2547051958837116079.wt
data/mongo 内为 WiredTiger 引擎的 .wt 数据文件与 WiredTiger* 元数据;data/typesense 为 Typesense 的索引段文件。valkey 未做持久化配置,仅作 10 秒级用户会话缓存,重启丢失无业务影响。
💡 技术拓展:Docker 数据持久化有三种常见形态——
形态 写法 特点 适用 bind mount ./data:/data/db数据在宿主指定目录,可直接备份/迁移 需要掌控数据位置(本文) 命名卷 mm-mongo:/data/db由 Docker 管理于 /var/lib/docker/volumes,隔离性好通用场景、K8s 外单机 tmpfs/无挂载 不配置 重启即失 纯缓存类服务(本文 valkey) 官方 Wiki 样例即用 bind mount,本文沿用;好处是
tar/rsync直接对data/目录做文件级容灾(数据库一致性仍需 mongodump,见 5.2)。
5.2 逻辑备份(mongodump)与恢复验证
文件级拷贝对运行中的 MongoDB 不保证一致性,官方口径用 mongodump --gzip --archive。实测备份全库:
docker compose exec -T mongo mongodump \
--uri "mongodb://root:q1w2e3r4@localhost:27017/manage_meals" \
--authenticationDatabase admin --gzip --archive > manage-meals-backup.tar.gz
2026-11-01T18:47:09.482+0000 writing `manage_meals.tags` to `archive on stdout`
2026-11-01T18:47:09.483+0000 writing `manage_meals.syncs` to `archive on stdout`
...
2026-11-01T18:47:09.501+0000 done dumping `manage_meals.users` (1 document)
ls -l manage-meals-backup.tar.gz
-rw-r--r-- 1 root root 4402 Nov 2 02:47 manage-meals-backup.tar.gz
7 个集合各 1 文档、4402 字节。恢复验证(先恢复到临时库 mm_restore 确认数据完好,再删除,不动生产库):
docker compose exec -T mongo sh -c 'mongorestore \
--uri "mongodb://root:q1w2e3r4@localhost:27017" --authenticationDatabase admin \
--gzip --archive --nsFrom="manage_meals.*" --nsTo="mm_restore.*" --drop' \
< manage-meals-backup.tar.gz | tail -2
2026-11-01T18:48:04.787+0000 restoring indexes for collection `mm_restore.tags` from metadata
2026-11-01T18:48:04.799+0000 7 document(s) restored successfully. 0 document(s) failed to restore.
docker compose exec -T mongo mongosh -u root -p q1w2e3r4 --authenticationDatabase admin --quiet \
--eval 'const d=db.getSiblingDB("mm_restore"); JSON.stringify(d.getCollectionNames())+" recipes="+d.recipes.countDocuments()'
docker compose exec -T mongo mongosh -u root -p q1w2e3r4 --authenticationDatabase admin --quiet \
--eval 'db.getSiblingDB("mm_restore").dropDatabase()'
["cookbooks","syncs","tags","recipes","mealplans","users","shoppinglists"] recipes=1
{ ok: 1, dropped: 'mm_restore' }
7 文档全部恢复、tags 唯一索引随备份还原,备份链路可用。正式恢复生产库时把 --nsTo 改回 manage_meals.* 即可。
六、环境变量表
Compose 文件里实际注入的环境变量按服务汇总如下(含义详解见第七章)。
6.1 manage-meals-api(12 项)
| 变量 | 实测值 | 说明 |
|---|---|---|
MONGO_URL | mongodb://root:q1w2e3r4@mongo:27017/ | MongoDB 连接串(含凭据) |
MONGO_DB | manage_meals | 业务库名 |
REDIS_URL | redis://valkey:6379 | 缓存连接(Valkey 兼容 Redis 协议) |
TYPESENSE_HOST / TYPESENSE_PORT | typesense / 8108 | 搜索引擎地址 |
TYPESENSE_API_KEY | q1w2e3r4 | Typesense 主密钥 |
ACCESS_JWT_SECRET | access-jwt-secret-q1w2e3r4 | accessToken 签名密钥 |
REFRESH_JWT_SECRET | refresh-jwt-secret-q1w2e3r4 | refreshToken 签名密钥 |
AUTH_ACCESS_TOKEN_EXPIRE_SEC | 600 | accessToken 有效期(秒) |
AUTH_REFRESH_TOKEN_EXPIRE_SEC | 2629746 | refreshToken 有效期(≈30 天) |
RECIPE_SCRAPER_URL | http://recipe-scraper:8000 | URL 导入抓取器地址 |
USER_REGISTER_ENABLED | "true" | 开放注册开关 |
6.2 manage-meals-web(9 项)
| 变量 | 实测值 | 说明 |
|---|---|---|
PUBLIC_MAIN_TITLE | ManageMeals | 站点标题(PUBLIC_ 前缀为 SvelteKit 公开变量) |
API_URL | http://manage-meals-api:3000/v1 | 后端 API 地址(SSR 内部调用) |
ORIGIN | http://localhost:8146 | 站点对外地址,必须与浏览器实际访问地址一致 |
COOKIE_ACCESS_TOKEN / COOKIE_REFRESH_TOKEN | mmeals_access_token / mmeals_refresh_token | 登录态 Cookie 名 |
COOKIE_ACCESS_TOKEN_EXPIRE_SEC / COOKIE_REFRESH_TOKEN_EXPIRE_SEC | 600 / 2629746 | Cookie 有效期,应与 API 侧一致 |
PASSWORD_MIN_LENGTH | 6 | 前端注册密码最短长度 |
BODY_SIZE_LIMIT | Infinity | SvelteKit 请求体上限(图片上传走 S3 时不受此限) |
6.3 mongo / mongo-express / typesense / search-sync
| 服务 | 变量/参数 | 实测值 | 说明 |
|---|---|---|---|
| mongo | MONGO_INITDB_ROOT_USERNAME / MONGO_INITDB_ROOT_PASSWORD | root / q1w2e3r4 | 初始超级用户 |
| mongo | MONGO_INITDB_DATABASE | manage_meals | 初始库名 |
| mongo-express | ME_CONFIG_MONGODB_ADMINUSERNAME / ME_CONFIG_MONGODB_ADMINPASSWORD / ME_CONFIG_MONGODB_URL | root / q1w2e3r4 / 连接串 | 连库凭据 |
| mongo-express | ME_CONFIG_BASICAUTH | "false" | 关闭管理台自身登录(生产务必开启或撤掉本服务) |
| typesense | command | --data-dir /data --api-key=q1w2e3r4 --enable-cors | 数据目录/主密钥/CORS |
| search-sync | MONGO_URL MONGO_DB TYPESENSE_HOST/PORT/API_KEY | 同 API 侧 | 同步器直连 Mongo 写 Typesense |
注入是否生效,用容器内实际环境验证:
docker compose exec -T manage-meals-api printenv | grep -E '^(MONGO|REDIS|TYPESENSE|ACCESS|REFRESH|AUTH_|RECIPE_|USER_)' | sort
ACCESS_JWT_SECRET=access-jwt-secret-q1w2e3r4
AUTH_ACCESS_TOKEN_EXPIRE_SEC=600
AUTH_REFRESH_TOKEN_EXPIRE_SEC=2629746
MONGO_DB=manage_meals
MONGO_URL=mongodb://root:q1w2e3r4@mongo:27017/
RECIPE_SCRAPER_URL=http://recipe-scraper:8000
REDIS_URL=redis://valkey:6379
REFRESH_JWT_SECRET=refresh-jwt-secret-q1w2e3r4
TYPESENSE_API_KEY=q1w2e3r4
TYPESENSE_HOST=typesense
TYPESENSE_PORT=8108
USER_REGISTER_ENABLED=true
七、配置参数说明
本节解释上表关键参数的行为与取值依据,均为实测或源码确认。
7.1 JWT 双令牌与有效期
ACCESS_JWT_SECRET/REFRESH_JWT_SECRET 分别为两类令牌签名密钥;实测登录返回的 accessToken 解码后 iat=1793558624, exp=1793559224,差值恰为 AUTH_ACCESS_TOKEN_EXPIRE_SEC=600;refreshToken exp=1796188370,差值 2629746 秒≈30 天,与配置一致。令牌过期后用旧 accessToken 调 API 返回 {"statusCode":401,...,"message":"Invalid Authorization header"},续期接口为 POST /v1/auth/refresh-token,请求字段名是 token(传 refreshToken 会 400):
curl -s -X POST http://localhost:8148/v1/auth/refresh-token \
-H 'Content-Type: application/json' -d '{"token":"<refreshToken>"}'
返回新的 accessToken。Web 端由 Cookie 自动完成该续期,API 直连用户需自行处理。两个 JWT 密钥是生产环境必改项:泄露即可伪造任意用户令牌。
7.2 USER_REGISTER_ENABLED
注册开关。设为 "false" 后 /v1/auth/register 拒绝新注册,适合已建好账号后关闭公网自助注册。实测本环境为 "true",4.6 注册成功。
7.3 ORIGIN 与 Cookie 一致性
ORIGIN 决定 Web 端 Set-Cookie 的作用域与 SSR 自请求地址。浏览器以 http://localhost:8146 访问时实测 Cookie 正常、未登录跳转为 307 带 goto(4.4)。若通过域名/反代访问(如 https://meals.example.com),必须把 ORIGIN 改为该地址,否则 Cookie 域不匹配导致登录后仍跳登录页。COOKIE_*_EXPIRE_SEC 应与 API 侧 AUTH_*_EXPIRE_SEC 保持一致,避免 Cookie 未过期而令牌已过期的空窗。
7.4 数据源与缓存
MONGO_URL 含账号密码,仅走内部网络 mongo:27017;MONGO_DB 默认 manage_meals。REDIS_URL 指向 valkey,API 用它缓存用户鉴权结果(源码为 10 秒 TTL),重启清空无业务影响。
7.5 搜索链路(TYPESENSE_* 与 search-sync)
API 的 /v1/search 查询 Typesense recipes 集合;集合由 search-sync 首次运行时自动创建(4.5 实测 default_sorting_field: rating)。search-sync 源码内置 cron.schedule("*/3 * * * *"):每 3 分钟按 updatedAt >= lastSyncedAt 增量拉取 MongoDB 写入索引,水位记录在 syncs 集合。实测日志:
search-sync-1 | Starting search sync
search-sync-1 | Imported 1 recipes
search-sync-1 | Finished search sync
这就是 4.7 中「导入后搜索要等约 3 分钟」的原因;q/c/p 三个查询参数必填(c 当前仅支持 recipes)。
7.6 抓取器链路(RECIPE_SCRAPER_URL 与 scraper 可选项)
API 通过 RECIPE_SCRAPER_URL 调用 scraper 完成 URL→菜谱解析。scraper 零配置可用,可选环境变量(源码默认值):PPLX_API_KEY(默认空;配置后冷门站点走 Perplexity AI 兜底解析)、DEFAULT_RECIPE_IMG(默认官方占位图 URL)、SCRAPER_CACHE_TIMEOUT(默认 10)、SCRAPER_PROXY_URL(默认空;抓受限站点时配代理)、CACHE_REDIS_HOST/PORT/DB/URL/KEY_PREFIX(可选,配 scraper 自身结果缓存)。直连验证:
curl -s "http://localhost:8153?url=https://www.bbcgoodfood.com/recipes/best-ever-chocolate-brownies-recipe" | head -c 200
{"author":"Orlando Murrin","canonical_url":"https://bbcgoodfood.com/recipes/best-ever-chocolate-brownies-recipe","category":"Afternoon tea, Dessert, Treat","cook_time":"35","cuisine":"American","description"...
7.7 未注入参数走内置默认
API 源码 .env 样例共 58 个参数(SMTP、S3、OAuth、支付、分页等),最小部署不注入即取代码默认值。实测例证:未设 ITEMS_PER_PAGE,GET /v1/recipes 回显 "perPage":20(样例默认值 20);未设 PASSWORD_MIN_LENGTH 时 API 侧默认 6,与 Web 侧显式注入值一致。完整 58 参数的分类详解与生产模板见本专栏的配置参数详解篇。
八、配置文件挂载
ManageMeals 官方镜像没有传统配置文件挂载点——API/Web 的全部配置走环境变量(第六、七章),不存在 app.yaml/nginx.conf 之类需要挂载的文件;唯一的「配置载体」就是 compose 文件本身。因此配置管理的正确姿势是:
-
compose 文件即配置:纳入 Git 管理,改配置 = 改
environment+docker compose up -d(只重建受影响服务)。语法校验命令实测:docker compose config --quiet && echo 'config ok (exit 0)'config ok (exit 0) -
敏感值用
.env插值:compose v2 自动加载同目录.env,可把MONGO_URL、JWT 密钥等写成${MONGO_URL}插值(Compose 通用机制;本实测环境按演示要求直接内联明文值,未启用插值)。生产建议.env权限600且不入库。 -
API 全量参数样例文件:后端仓库提供
api/.env.example(58 项,本实验环境已取到样例原文),若需要启用邮件验证/S3 存储/OAuth 登录等高级能力,把对应键加进 compose 的environment即可,无需挂载文件。
九、等价 docker run 命令
Compose 是官方推荐形态;为便于单服务排障或理解 compose 到 docker run 的映射,给出等价改写。其中 valkey 一项已按该形态单独实测:
docker network create --subnet 10.89.63.0/24 manage-meals-net # compose 已自动创建,勿重复
docker run -d --name mm-valkey-check --network manage-meals-net valkey/valkey:8
docker exec mm-valkey-check valkey-cli ping
docker rm -f mm-valkey-check
PONG
mm-valkey-check
其余服务等价改写(与 4.1 compose 一一对应,未单独实测,主线仍以 compose 为准):
docker run -d --name mm-mongo --network manage-meals-net \
-e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=q1w2e3r4 \
-e MONGO_INITDB_DATABASE=manage_meals \
-v /opt/manage-meals/data/mongo:/data/db mongo:7
docker run -d --name mm-typesense --network manage-meals-net -p 8151:8108 \
-v /opt/manage-meals/data/typesense:/data \
typesense/typesense:28.0 --data-dir /data --api-key=q1w2e3r4 --enable-cors
docker run -d --name mm-scraper --network manage-meals-net -p 8153:8000 \
ghcr.io/managemeals/manage-meals-scraper:latest
docker run -d --name mm-search-sync --network manage-meals-net \
-e MONGO_URL=mongodb://root:q1w2e3r4@mongo:27017/ -e MONGO_DB=manage_meals \
-e TYPESENSE_API_KEY=q1w2e3r4 -e TYPESENSE_HOST=typesense -e TYPESENSE_PORT=8108 \
ghcr.io/managemeals/manage-meals-search-sync:latest
docker run -d --name mm-api --network manage-meals-net -p 8148:3000 \
-e MONGO_URL=mongodb://root:q1w2e3r4@mongo:27017/ -e MONGO_DB=manage_meals \
-e REDIS_URL=redis://valkey:6379 \
-e TYPESENSE_API_KEY=q1w2e3r4 -e TYPESENSE_HOST=typesense -e TYPESENSE_PORT=8108 \
-e ACCESS_JWT_SECRET=access-jwt-secret-q1w2e3r4 -e REFRESH_JWT_SECRET=refresh-jwt-secret-q1w2e3r4 \
-e AUTH_ACCESS_TOKEN_EXPIRE_SEC=600 -e AUTH_REFRESH_TOKEN_EXPIRE_SEC=2629746 \
-e RECIPE_SCRAPER_URL=http://recipe-scraper:8000 -e USER_REGISTER_ENABLED=true \
ghcr.io/managemeals/manage-meals-api:latest
docker run -d --name mm-web --network manage-meals-net -p 8146:3000 \
-e PUBLIC_MAIN_TITLE=ManageMeals -e PASSWORD_MIN_LENGTH=6 \
-e API_URL=http://manage-meals-api:3000/v1 \
-e COOKIE_ACCESS_TOKEN=mmeals_access_token -e COOKIE_REFRESH_TOKEN=mmeals_refresh_token \
-e COOKIE_ACCESS_TOKEN_EXPIRE_SEC=600 -e COOKIE_REFRESH_TOKEN_EXPIRE_SEC=2629746 \
-e ORIGIN=http://localhost:8146 -e BODY_SIZE_LIMIT=Infinity \
ghcr.io/managemeals/manage-meals-web:latest
⚠️ 注意:两种形态二选一。混跑会因服务名(DNS)相同冲突,例如
mm-api与 compose 的manage-meals-api-1都注册别名manage-meals-api时内部解析将不确定。
十、常用运维命令
以下命令均在 /opt/manage-meals 目录实测。
10.1 日常查看
docker compose ps # 服务状态(见 4.3 回显)
docker compose logs -f manage-meals-api # 跟踪 API 日志(pino JSON 行)
docker compose top # 各服务进程树
API 日志为结构化 JSON,实测节选:
manage-meals-api-1 | {"level":30,"time":1793558461236,...,"url":"/v1/auth/login","id":"req-1b","msg":"received request"}
manage-meals-api-1 | {"level":30,"time":1793558461274,...,"url":"/v1/auth/login","statusCode":200,"msg":"request completed"}
10.2 升级(官方口径 pull + up)
docker compose pull
Image ghcr.io/managemeals/manage-meals-api:latest Pulling
...
Image ghcr.io/managemeals/manage-meals-api:latest Pulled
Image typesense/typesense:28.0 Pulled
docker compose up -d
pull 取回新 :latest 后,up -d 只重建镜像有变化的服务,bind mount 数据不动。维护者声明镜像向后兼容;因 :latest 不可回退,升级前建议先做 5.2 的 mongodump 备份,并记录旧 digest 以便回滚(把 compose 的 image 改为旧 digest 再 up -d)。
10.3 单服务重启与进入
docker compose restart search-sync
Container manage-meals-v193-stack-search-sync-1 Restarting
Container manage-meals-v193-stack-search-sync-1 Started
docker compose exec manage-meals-api sh # 进入 API 容器排障
docker compose exec mongo mongosh -u root -p q1w2e3r4 --authenticationDatabase admin
10.4 停止与卸载
docker compose down
Container manage-meals-v193-stack-manage-meals-web-1 Stopping
...
Network manage-meals-net Removing
Network manage-meals-net Removed
down 删除容器与网络但保留 data/ 目录数据(bind mount 不受影响),up -d 可原样恢复;彻底卸载 = down 后再删除 /opt/manage-meals 目录与 docker rmi 相应镜像。
十一、常见问题
Q1:按版本号拉镜像报 not found?
实测:docker pull ghcr.io/managemeals/manage-meals-api:v1.9.3 → Error response from daemon: failed to resolve reference ...: not found。官方只发 :latest,无版本 tag。用 2.2 的方法拉 :latest 后以 digest 固定版本。
Q2:URL 导入返回 400 Bad Request?
两种成因,回显不同。其一,参数位置错:url 必须放 querystring,放 JSON body 实测返回 {"statusCode":400,"code":"FST_ERR_VALIDATION","message":"querystring must have required property 'url'"}(见 4.7 注意框)。其二,目标站点解析失败:API 返回 400 "We couldn't import a recipe from this URL...",查 scraper 日志可见根因,实测一例:
recipe-scrapers (15.12.0) exception: No Recipe Schema found
(对应 URL 的 slug 在目标站不存在/站点无结构化菜谱标注。)处理:核对 URL 有效性;冷门站点可给 scraper 配 PPLX_API_KEY 走 AI 兜底(7.6),或手动录入。
Q3:导入成功但搜索搜不到?
search-sync 为 3 分钟 cron 增量同步(7.5),导入后立即查询 found:0 属预期。实测等待约 3 分钟,日志出现 Imported 1 recipes 后 found:1。若持续搜不到,检查 search-sync 容器是否存活、TYPESENSE_* 三参数与 API 侧是否一致。
Q4:昨天能用的 accessToken 今天 401?
accessToken 有效期仅 AUTH_ACCESS_TOKEN_EXPIRE_SEC=600 秒,过期实测返回 Invalid Authorization header。用 refreshToken 调 POST /v1/auth/refresh-token(字段名 token)换新令牌(7.1);Web 端自动处理,无需干预。
Q5:创建标签返回 500?
重复创建同名标签时 API 返回 500,日志实测为 MongoDB 唯一索引冲突:E11000 duplicate key error collection: manage_meals.tags index: slug_1_createdByUuid_1 dup key: { slug: "dessert", ... }。属数据约束生效,换名即可;官方 Wiki 要求 tags 集合建 uuid 唯一与 slug+createdByUuid 复合唯一两个索引,索引缺失会导致重复数据入库。
Q6:登录后仍跳回登录页/307 循环?
优先核对 ORIGIN 是否与浏览器实际访问地址完全一致(协议+主机+端口,7.3),以及 COOKIE_*_EXPIRE_SEC 与 API 侧 AUTH_*_EXPIRE_SEC 是否对齐。实测正确配置下未登录访问详情页返回 307 且带 goto 回跳参数(4.4),登录一次即回原页面。
Q7:ghcr.io 拉取超时?
国内直连 ghcr 常不可达。为 Docker 配置 HTTP 代理(/etc/docker/daemon.json 的 proxies 或 systemd 环境)后实测全部镜像直拉成功;raw.githubusercontent.com 的样例文件下载同样间歇失败,重试即可。
十二、参考链接
- ManageMeals 官网:https://managemeals.com/ (查询日期 2026-11-02)
- 官方自托管 Wiki(compose 样例来源):https://github.com/managemeals/manage-meals-api/wiki/Self%E2%80%90hosting (查询日期 2026-11-02)
- 后端仓库(v1.9.3、api/.env.example):https://github.com/managemeals/manage-meals-api/releases (查询日期 2026-11-02)
- 前端仓库(v1.7.3):https://github.com/managemeals/manage-meals-web/releases (查询日期 2026-11-02)
- 官方镜像仓库:https://github.com/orgs/managemeals/packages (查询日期 2026-11-02)
- Docker Compose 文档:https://docs.docker.com/compose/ (查询日期 2026-11-02)
- 站内:《ManageMeals 介绍与选型指南》