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)

ManageMealsManageMealsDocker ComposeMongoDBValkeyTypesense2026/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 Composev5.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-webghcr.io/managemeals/manage-meals-web:latest587MBSvelteKit 前端(SSR,监听 3000)
manage-meals-apighcr.io/managemeals/manage-meals-api:latest479MBFastify 后端 API(监听 3000)
recipe-scraperghcr.io/managemeals/manage-meals-scraper:latest174MBPython/Flask 菜谱 URL 抓取器(监听 8000)
search-syncghcr.io/managemeals/manage-meals-search-sync:latest236MBMongoDB→Typesense 搜索索引同步器
mongomongo:71.18GB主数据库(库名 manage_meals)
valkeyvalkey/valkey:8170MBRedis 兼容缓存(API 会话/缓存)
typesensetypesense/typesense:28.01.19GB全文搜索引擎(监听 8108)
mongo-expressmongo-express287MBMongoDB 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_URLmongodb://root:q1w2e3r4@mongo:27017/MongoDB 连接串(含凭据)
MONGO_DBmanage_meals业务库名
REDIS_URLredis://valkey:6379缓存连接(Valkey 兼容 Redis 协议)
TYPESENSE_HOST / TYPESENSE_PORTtypesense / 8108搜索引擎地址
TYPESENSE_API_KEYq1w2e3r4Typesense 主密钥
ACCESS_JWT_SECRETaccess-jwt-secret-q1w2e3r4accessToken 签名密钥
REFRESH_JWT_SECRETrefresh-jwt-secret-q1w2e3r4refreshToken 签名密钥
AUTH_ACCESS_TOKEN_EXPIRE_SEC600accessToken 有效期(秒)
AUTH_REFRESH_TOKEN_EXPIRE_SEC2629746refreshToken 有效期(≈30 天)
RECIPE_SCRAPER_URLhttp://recipe-scraper:8000URL 导入抓取器地址
USER_REGISTER_ENABLED"true"开放注册开关

6.2 manage-meals-web(9 项)

变量实测值说明
PUBLIC_MAIN_TITLEManageMeals站点标题(PUBLIC_ 前缀为 SvelteKit 公开变量)
API_URLhttp://manage-meals-api:3000/v1后端 API 地址(SSR 内部调用)
ORIGINhttp://localhost:8146站点对外地址,必须与浏览器实际访问地址一致
COOKIE_ACCESS_TOKEN / COOKIE_REFRESH_TOKENmmeals_access_token / mmeals_refresh_token登录态 Cookie 名
COOKIE_ACCESS_TOKEN_EXPIRE_SEC / COOKIE_REFRESH_TOKEN_EXPIRE_SEC600 / 2629746Cookie 有效期,应与 API 侧一致
PASSWORD_MIN_LENGTH6前端注册密码最短长度
BODY_SIZE_LIMITInfinitySvelteKit 请求体上限(图片上传走 S3 时不受此限)

6.3 mongo / mongo-express / typesense / search-sync

服务变量/参数实测值说明
mongoMONGO_INITDB_ROOT_USERNAME / MONGO_INITDB_ROOT_PASSWORDroot / q1w2e3r4初始超级用户
mongoMONGO_INITDB_DATABASEmanage_meals初始库名
mongo-expressME_CONFIG_MONGODB_ADMINUSERNAME / ME_CONFIG_MONGODB_ADMINPASSWORD / ME_CONFIG_MONGODB_URLroot / q1w2e3r4 / 连接串连库凭据
mongo-expressME_CONFIG_BASICAUTH"false"关闭管理台自身登录(生产务必开启或撤掉本服务)
typesensecommand--data-dir /data --api-key=q1w2e3r4 --enable-cors数据目录/主密钥/CORS
search-syncMONGO_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 注册成功。

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 文件本身。因此配置管理的正确姿势是:

  1. compose 文件即配置:纳入 Git 管理,改配置 = 改 environment + docker compose up -d(只重建受影响服务)。语法校验命令实测:

    docker compose config --quiet && echo 'config ok (exit 0)'
    
    config ok (exit 0)
    
  2. 敏感值用 .env 插值:compose v2 自动加载同目录 .env,可把 MONGO_URL、JWT 密钥等写成 ${MONGO_URL} 插值(Compose 通用机制;本实测环境按演示要求直接内联明文值,未启用插值)。生产建议 .env 权限 600 且不入库。

  3. 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 Docker Compose 部署指南 - Quzuo