RecipeSage Docker Compose 部署指南
RecipeSage v4.1.1 官方 RecipeSage-selfhost 六服务栈(proxy/static/api/pushpin/postgres/valkey)在 Ubuntu 24.04/Docker 29.9.2 实测部署:compose 全文与端口 8162 改造、Prisma 69 迁移自动执行、注册/建谱/购物清单/双账号好友协作/JSON-LD 导出/40 表 pg_dump 备份全链路逐字回显、616MiB 资源实测、环境变量表与 8 条 FAQ(实测日期 2026-11-02)
RecipeSage Docker Compose 部署指南
目标:在一台干净的 Ubuntu 24.04 服务器上,用官方
RecipeSage-selfhostcompose 从零部署 RecipeSage v4.1.1 六服务栈(proxy/static/api/pushpin/postgres/valkey),完成注册、建食谱、协作购物清单、JSON-LD 导出与数据库备份的端到端闭环。全部命令与回显均来自实测环境(Ubuntu 24.04.5 LTS / x86_64 / Docker 29.9.2 / Docker Compose v5.6.1,实测日期 2026-11-02)。组件功能与选型评估见站内文章 《RecipeSage 介绍与选型指南》。⚠️ 三条首要纠偏:① 部署仓库是
julianpoy/RecipeSage-selfhost(源码仓库julianpoy/RecipeSage,不存在 RecipeSage/RecipeSage);② v4 数据库是 PostgreSQL 16,不是旧版的 MongoDB,前端是 Angular,不是 Ember;③ 官方 compose 默认钉 v4.1.0,本文按最新稳定 v4.1.1 升级部署(tag 实测存在、可拉取)。⚠️ 验收方式说明:本次实测环境无浏览器自动化工具,前端页面验收降级为「HTTP 状态码 + 页面
<title>+ REST API 全链路实测」,未做像素级 UI 走查;功能链路(注册/建谱/清单/协作/导出)均通过 API 实测闭环。
一、环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 24.04.5 LTS(x86_64) |
| Docker 引擎 | 29.9.2 |
| Docker Compose | v5.6.1(docker compose 子命令形态) |
| 内存 | 15.57GiB(栈空载常驻约 616MiB,见 4.5) |
| 应用镜像 | julianpoy/recipesage-selfhost:api-v4.1.1(解包 3.21GB/压缩 588MB)、julianpoy/recipesage-selfhost:static-v4.1.1(268MB/72MB) |
| 基础镜像 | julianpoy/recipesage-selfhost-proxy:2026-08-16(nginx)、julianpoy/pushpin:2023-09-17、postgres:16、valkey/valkey |
| 对外端口 | 宿主 8162 → proxy 容器 80(本批次 8161 已被其他组件占用,改用 8162) |
| compose 项目名 | recipesage(-p recipesage,卷/网络前缀由此而来) |
| 数据卷 | recipesage_apimedia、recipesage_postgresdata |
| 演示账号 | 邮箱 admin@example.com、friend@example.com,密码均 q1w2e3r4 |
| 镜像源 | ⚠️ 实测环境直连 Docker Hub 受限,全部镜像经国内镜像加速源(docker.1ms.run 前缀)拉取成功;有直连条件的服务器按原 tag 拉取即可 |
二、镜像选型说明
RecipeSage 官方为自托管发布成对版本 tag:api-vX.Y.Z(后端)与 static-vX.Y.Z(前端),必须同版本成对升级,否则前后端 API 形状可能错位。GitHub Releases 实测版本线:v4.1.1(2026-10-31)→ v4.1.0(10-26)→ v4.0.19(10-25)。
| 服务 | 镜像 | 版本策略 |
|---|---|---|
| api | julianpoy/recipesage-selfhost:api-v4.1.1 | 与应用版本严格成对;官方 compose 默认 v4.1.0,本文已升 v4.1.1 |
| static | julianpoy/recipesage-selfhost:static-v4.1.1 | 同上 |
| proxy | julianpoy/recipesage-selfhost-proxy:2026-08-16 | 按日期 tag 钉定,独立于应用版本 |
| pushpin | julianpoy/pushpin:2023-09-17 | GRIP 代理,日期 tag,长期未变 |
| postgres | postgres:16 | 官方钉 16 主版本 |
| valkey | valkey/valkey | 官方 compose 未钉 tag(实测解析为 latest);生产建议自钉稳定版本 |
💡 技术拓展:队列/缓存组件这里用了 Valkey——它是 Redis 因许可证变更(BSL→RSALv2/SSPLv1)后由 Linux 基金会主导的开源分叉,协议与命令与 Redis 7.x 兼容;同类可选还有
redis:7(旧 BSD 版为 7.2.4 以前)与 KeyDB。RecipeSage compose 通过JOB_QUEUE_REDIS_HOST指向它,官方直接换用 Valkey 镜像,说明走的是纯 Redis 协议兼容路径,无 Valkey 专属特性依赖——如需换回 redis 镜像亦可行。
三、前置条件
3.1 Docker 与 Compose 版本
docker version --format 'Server: {{.Server.Version}}'
docker compose version
实测回显:
Server: 29.9.2
Docker Compose version v5.6.1
3.2 端口与资源
本实例宿主对外仅占用 8162 一个端口(proxy 容器 80 映射),其余服务只在 compose 内部网络互通、不映射宿主端口。部署前用 docker ps --format '{{.Ports}}' 确认 8162 空闲即可;资源按 616MiB 常驻 + 3.5GB 镜像磁盘预留。
四、部署步骤
4.1 目录结构
recipesage/
└── docker-compose.yml
官方仓库 julianpoy/RecipeSage-selfhost 的 compose 配置版本为 2026-10-29。selfhost 形态没有 .env 文件,全部配置内联在 compose 的 x-common-env 锚点段(改配置=改 compose 文件,详见同专栏《RecipeSage 4.1.1 配置参数详解》)。
4.2 docker-compose.yml(实测可用全文)
相对官方原版的修改点仅三处:端口 7270→8162、API_PUBLIC_BASE_URL 指向本地地址、static/api 镜像 v4.1.0→v4.1.1(另为 valkey 补了 container_name)。
# This config file is for RecipeSage-selfhost config version 2026-10-29, and RecipeSage version 4.1.1.
x-common-env: &common-env
# === THESE VALUES MUST BE CHANGED ===
API_PUBLIC_BASE_URL: http://127.0.0.1:8162/api # host+port with /api suffix
# === THESE VALUES MAY OPTIONALLY BE CHANGED, BUT DO NOT NEED TO BE ===
GRIP_KEY: recipesage_selfhost # Pushpin is only exposed within the docker network, so you can leave this as-is if desired.
POSTGRES_DB: recipesage_selfhost # If changing this, make sure to update the DATABASE_URL below accordingly
POSTGRES_USER: recipesage_selfhost # If changing this, make sure to update the DATABASE_URL below accordingly
POSTGRES_PASSWORD: recipesage_selfhost # If changing this, make sure to update the DATABASE_URL below accordingly
DATABASE_URL: postgresql://recipesage_selfhost:recipesage_selfhost@postgres:5432/recipesage_selfhost # Format is: postgresql://USERNAME:PASSWORD@HOST:PORT/DBNAME and must match the variables above
GROCERY_CATEGORIZER_URL: https://grocery-categorizer.recipesage.com/ # Set to the local grocery-categorizer service if you uncomment it below
AI_PROVIDER: "openrouter" # Please follow the instructions in the README if you decide to supply a value here
AI_API_KEY: "" # Please follow the instructions in the README if you decide to supply a value here
AI_API_BASE_URL: "" # This does not need to be changed unless you are self-hosting your own LLM.
AI_MODEL_WEBPAGE: google/gemini-2.5-flash-lite # This model is recommended for this purpose
AI_MODEL_TEXT: google/gemini-2.5-flash-lite # This model is recommended for this purpose
AI_MODEL_OCR: google/gemini-2.5-flash # This model is recommended for this purpose
AI_MODEL_DOCUMENT: google/gemini-2.5-flash # This model is recommended for this purpose
AI_MODEL_MODERATION: google/gemini-2.5-flash # This model is recommended for this purpose
AI_MODEL_VISION: anthropic/claude-sonnet-4.6 # This model is recommended for this purpose
AI_MODEL_NUTRITION: google/gemini-2.5-flash-lite # This model is recommended for this purpose
AI_MODEL_ASSISTANT: anthropic/claude-sonnet-4.6 # Use whatever model you prefer
AI_MODEL_ASSISTANT_LOW: google/gemini-2.5-flash # Use whatever model you prefer
SCRAPFLY_API_KEY: "" # Can be provided to bypass CloudFlare anti-bot for better clip from URL support. Or, just use the browser extension instead.
# === Please do not change any values below this line ===
services:
proxy: # The proxy must not be removed. If needed, point your own proxy to this container, rather than removing this
container_name: recipesage_proxy
image: julianpoy/recipesage-selfhost-proxy:2026-08-16
ports:
- 8162:80
depends_on:
- static
- api
- pushpin
restart: unless-stopped
static: # Hosts frontend assets
container_name: recipesage_static
image: julianpoy/recipesage-selfhost:static-v4.1.1
restart: unless-stopped
api: # Hosts backend API
container_name: recipesage_api
image: julianpoy/recipesage-selfhost:api-v4.1.1
depends_on:
postgres:
condition: service_healthy
pushpin:
condition: service_started
valkey:
condition: service_started
command: ./scripts/selfhost-startup.sh
environment:
<<: *common-env
# Please don't change these environment variables
ENVIRONMENT: selfhost
NODE_ENV: selfhost
NODE_OPTIONS: "--max-old-space-size=400"
MALLOC_ARENA_MAX: "2"
FILESYSTEM_STORAGE_PATH: /rs-media
STORAGE_TYPE: filesystem
VERBOSE: "false"
VERSION: selfhost
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_SSL: "false"
POSTGRES_LOGGING: "false"
DATABASE_SSL: "false"
GRIP_URL: http://pushpin:5561/
JOB_QUEUE_REDIS_HOST: valkey
STRIPE_SK: "" # Value should not be set.
GCM_KEYPAIR: "" # Value should not be set.
SENTRY_DSN: "" # Value should not be set.
STRIPE_WEBHOOK_SECRET: "" # Value should not be set
volumes:
- apimedia:/rs-media
restart: unless-stopped
pushpin: # Provides websocket support
container_name: recipesage_pushpin
image: julianpoy/pushpin:2023-09-17
entrypoint: /bin/sh -c
command:
[
'sed -i "s/sig_key=changeme/sig_key=$$GRIP_KEY/" /etc/pushpin/pushpin.conf && echo "* $${TARGET},over_http" > /etc/pushpin/routes && pushpin --merge-output',
]
environment:
<<: *common-env
TARGET: api:3000
restart: unless-stopped
postgres: # Database
container_name: recipesage_postgres
image: postgres:16
environment:
<<: *common-env
volumes:
- postgresdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD", "pg_isready", "-d", "recipesage_selfhost", "-U", "recipesage_selfhost"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
valkey:
container_name: recipesage_valkey
restart: unless-stopped
image: valkey/valkey
volumes:
apimedia:
driver: local
postgresdata:
driver: local
要点解读:x-common-env 锚点被 api/pushpin/postgres 三方共享(同一份 DB 凭据与 AI 配置);postgres 带 pg_isready 健康检查,api 用 condition: service_healthy 等库就绪后才启动并自动迁移;proxy 是唯一映射宿主端口的服务,官方注释明确不可移除(外部反代应指向它,而不是绕过它直连 api/static)。
4.3 启动
docker compose -p recipesage up -d
实测回显(栈已运行时的幂等输出;首次启动会先显示各容器 Created/Started,并拉取约 4.5GB 镜像):
Container recipesage_api Running
Container recipesage_pushpin Running
Container recipesage_postgres Running
Container recipesage_valkey Running
Container recipesage_proxy Running
Container recipesage_static Running
Container recipesage_postgres Waiting
Container recipesage_postgres Healthy
验证六容器全部在线:
docker compose -p recipesage ps --format '{{.Name}} {{.Status}}'
实测回显:
recipesage_api Up 14 minutes
recipesage_postgres Up 14 minutes (healthy)
recipesage_proxy Up 14 minutes
recipesage_pushpin Up 14 minutes
recipesage_static Up 14 minutes
recipesage_valkey Up 14 minutes
4.4 api 启动序列与自动迁移
api 容器 command: ./scripts/selfhost-startup.sh,序列为 prisma migrate deploy → CLI seed → CLI indexRecipes → 启动后端,无需手工初始化数据库。首次启动日志实测节选(输出节选,实测首尾):
==== Running migrations, please wait. ====
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from packages/prisma/src/prisma.
Datasource "db": PostgreSQL database "recipesage_selfhost", schema "public" at "postgres:5432"
69 migrations found in prisma/migrations
Applying migration `0_init`
Applying migration `20231107015959_default_createdat`
Applying migration `20231112183754_create_assistantmessage`
...
迁移完成后开始服务请求,日志尾部可见实时请求行(节选):
POST /compat/v2/jobs/startExportJob 200 3.829 ms - -
Starting processing job 2 with EXPORT.jsonld
Finished processing job 2
Job queue has drained
Job queue has drained 即 Valkey 队列的导出 job 被 worker 消费完毕的证据。
4.5 资源实测
docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}' recipesage_proxy recipesage_static recipesage_api recipesage_pushpin recipesage_postgres recipesage_valkey
实测回显:
recipesage_proxy 6.754MiB / 15.57GiB 0.00%
recipesage_static 6.551MiB / 15.57GiB 0.00%
recipesage_api 516.7MiB / 15.57GiB 0.48%
recipesage_pushpin 35.09MiB / 15.57GiB 0.04%
recipesage_postgres 43.66MiB / 15.57GiB 3.27%
recipesage_valkey 7.27MiB / 15.57GiB 0.10%
合计约 616MiB,api 占 84%——其堆上限由 compose 注入的 NODE_OPTIONS=--max-old-space-size=400 控制,小内存机器可下调(见配置详解篇)。
五、页面与 API 验收
5.1 前端入口
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://127.0.0.1:8162/
curl -s http://127.0.0.1:8162/app/ -o rs_app.html && wc -c rs_app.html
curl -s http://127.0.0.1:8162/app/ | grep -o '<title>[^<]*</title>'
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8162/app/login
实测回显:
301 http://127.0.0.1/app/
15217 rs_app.html
<title>RecipeSage</title>
200
/ 被 nginx 301 到 /app/(Angular SPA,base href=/app/,meta robots noindex),页面壳 15217 字节,登录页 200。⚠️ 301 的 Location 不含宿主端口是容器内 nginx 按自身监听端口 80 生成的,浏览器跟随跳转时会保留原 Host:Port,不影响使用;如挂真实域名反代则无此现象。
5.2 API 欢迎页与自描述
curl -s http://127.0.0.1:8162/api/ | head -c 200
curl -s http://127.0.0.1:8162/api/openapi.json -o rs_openapi.json && wc -c rs_openapi.json
python3 -c "import json;d=json.load(open('rs_openapi.json'));print('title:',d['info']['title'],'| version:',d['info']['version'],'| paths:',len(d['paths']))"
实测回显:
<!DOCTYPE html><html><head><title></title><link rel="stylesheet" href="/stylesheets/style.css"></head><body><h1>RecipeSage API</h1><p>Welcome to the RecipeSage API<br>Usage of the RecipeSage API is free for personal, non-commercial use.
403081 rs_openapi.json
title: RecipeSage API | version: selfhost | paths: 144
openapi.json 由 tRPC 路由自动生成(文件内注明 "generated from RecipeSage's tRPC router"),144 个 path 是 REST 兼容层 /compat/v2/* 的权威清单;交互式文档在站外 docs.recipesage.com/api/(/api/docs 实测 404,属正常)。
5.3 nginx 路由表(recipesage_proxy 容器内 default.conf 实读)
| 路径 | 去向 | 说明 |
|---|---|---|
/ | 301 → /app/ | 前端入口 |
/app/ | static:80 | Angular SPA |
/api/、/app/api/ | api:3000 | 后端与 REST 兼容层 |
/grip/ws | pushpin:7999/ws | 实时推送 WebSocket |
/minio/ | $minio 变量 | 仅 S3/MinIO 存储形态启用,默认 filesystem 不走 |
manifest.json、service-worker.js | static | PWA 必须走根路径 |
六、业务闭环端到端验证
以下全部为 2026-11-02 实测命令与逐字回显。设 TOKEN 为 6.1 返回的 token。
6.1 注册主账号(无需邮箱验证)
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/users/register \
-H 'Content-Type: application/json' \
-d '{"name":"admin","email":"admin@example.com","password":"q1w2e3r4"}'
实测回显(HTTP 200):
{"token":"490d2318d743839953460fe010bc1a0ae5cfe8e2588bde69d74ac436ba1ccc71ef6a928226a9fe822cd4518b7814ccd7","userId":"42754089-a1d9-4e1d-8b04-7df237fbb337","email":"admin@example.com","isNewUser":true}
会话验证:
curl -s http://127.0.0.1:8162/api/compat/v2/users/validateSession -H "Authorization: Bearer $TOKEN"
实测回显:"Valid"(HTTP 200)。
6.2 创建食谱
⚠️ 实测踩坑:createRecipe 的 Zod schema 要求 title/description/yield/activeTime/totalTime/source/url/notes/instructions/ingredients/rating/folder/labelIds/imageIds 全部字段出现(缺任一报 400 invalid_type),folder 为枚举 main|inbox(传 null 报 invalid_value),时长用 ISO8601(PT10M)。完整请求:
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/recipes/createRecipe \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Pasta Aglio e Olio","description":"Garlic chili pasta","yield":"2 servings",
"activeTime":"PT10M","totalTime":"PT15M","source":"quzuo G25","url":"","notes":"keep starchy water",
"instructions":"1. Boil pasta.\n2. Sauté garlic and chili in olive oil.\n3. Toss pasta with garlic oil.",
"ingredients":"200 g spaghetti\n4 cloves garlic\n1 tsp chili flakes\n60 ml olive oil\n1 bunch parsley",
"rating":null,"folder":"main","labelIds":[],"imageIds":[]}'
实测回显:{"id":"fb231af7-5993-42d8-9bc8-e6a5862afaea"}(HTTP 200)。
列表验证(⚠️ getRecipes 是 POST,GET 实测 404 NOT_FOUND):
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/recipes/getRecipes \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"folder":"main","orderBy":"createdAt","orderDirection":"desc","offset":0,"limit":10}'
实测回显:
{"recipes":[{"id":"fb231af7-5993-42d8-9bc8-e6a5862afaea","userId":"42754089-a1d9-4e1d-8b04-7df237fbb337","fromUserId":null,"title":"Pasta Aglio e Olio","description":"Garlic chili pasta","yield":"2 servings","activeTime":"PT10M","totalTime":"PT15M","source":"quzuo G25","url":"","folder":"main","createdAt":"2026-11-01T20:54:59.373Z","updatedAt":"2026-11-01T20:54:59.373Z","lastMadeAt":null,"rating":null,"recipeLabels":[],"recipeImages":[],"fromUser":null,"user":{"id":"42754089-a1d9-4e1d-8b04-7df237fbb337","name":"admin","handle":null,"enableProfile":false,"profileImages":[]}}],"totalCount":1}
统计接口(⚠️ getMyStats 是 GET,POST 实测 404):
curl -s http://127.0.0.1:8162/api/compat/v2/users/getMyStats -H "Authorization: Bearer $TOKEN"
实测回显:{"recipeCount":1,"recipeImageCount":0,"messageCount":0,"createdAt":"2026-11-01T20:54:43.926Z","lastLogin":null}。
6.3 购物清单与条目
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/shoppingLists/createShoppingList \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Weekly Groceries","collaboratorUserIds":[]}'
实测回显:{"reference":"db793080-69fc-4845-8f88-f32b83cee172","id":"d0e7f44b-e764-435c-96f7-aca6d96bc637"}(⚠️ collaboratorUserIds 必填,缺省报 400 expected array, received undefined)。
加条目(shoppingListId 取上一步 id,recipeId 关联 6.2 的食谱):
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/shoppingLists/createShoppingListItem \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"shoppingListId":"d0e7f44b-e764-435c-96f7-aca6d96bc637","title":"spaghetti","quantity":"200 g","recipeId":"fb231af7-5993-42d8-9bc8-e6a5862afaea","completed":false,"categoryTitle":"Grains"}'
实测回显:{"reference":"c4763253-8f6c-4b93-ac70-2423fc46c752","id":"9c034266-d978-43cf-8818-75477903ca48"}(HTTP 200)。
6.4 双账号好友协作
注册第二账号:
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/users/register \
-H 'Content-Type: application/json' \
-d '{"name":"friend","email":"friend@example.com","password":"q1w2e3r4"}'
实测回显:{"token":"de4b2728834164ea6b22315e873361ca72e1147f565d3208f3d861cdd822e5c706a3133d04f2008288d3e6640c095773","userId":"1404d776-7b7f-4eb3-aa1e-552cd741e585","email":"friend@example.com","isNewUser":true}(HTTP 200)。
好友关系需双向建立(A 发起 + B 接受各调一次 createFriendship):
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/users/createFriendship \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"friendId":"1404d776-7b7f-4eb3-aa1e-552cd741e585"}'
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/users/createFriendship \
-H "Authorization: Bearer $FTOKEN" -H 'Content-Type: application/json' \
-d '{"friendId":"42754089-a1d9-4e1d-8b04-7df237fbb337"}'
两次实测回显均为 "Created"(HTTP 200)。A 侧确认:
curl -s http://127.0.0.1:8162/api/compat/v2/users/getMyFriends -H "Authorization: Bearer $TOKEN"
实测回显:{"friends":[{"id":"1404d776-7b7f-4eb3-aa1e-552cd741e585","name":"friend","handle":null,"enableProfile":false,"profileImages":[]}],"incomingRequests":[],"outgoingRequests":[]}。
把清单共享给 B(updateShoppingList 需带全 title 与 collaboratorUserIds):
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/shoppingLists/updateShoppingList \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"id":"d0e7f44b-e764-435c-96f7-aca6d96bc637","title":"Weekly Groceries","collaboratorUserIds":["1404d776-7b7f-4eb3-aa1e-552cd741e585"]}'
实测回显:{"reference":"c7a0c9ac-2232-46d8-ad94-ed6e9572e0c4","id":"d0e7f44b-e764-435c-96f7-aca6d96bc637"}(HTTP 200)。B 侧立即可见(协作闭环成立):
curl -s http://127.0.0.1:8162/api/compat/v2/shoppingLists/getShoppingLists -H "Authorization: Bearer $FTOKEN"
实测回显(节选):
[{"id":"d0e7f44b-e764-435c-96f7-aca6d96bc637","userId":"42754089-a1d9-4e1d-8b04-7df237fbb337","user":{"id":"42754089-a1d9-4e1d-8b04-7df237fbb337","name":"admin"},"collaboratorUsers":[{"user":{"id":"1404d776-7b7f-4eb3-aa1e-552cd741e585","name":"friend"}}],"title":"Weekly Groceries","_count":{"items":1}}]
⚠️ 纠偏:v4 已无旧群组 API——
GET/POST /api/groups实测均 404(Express 默认 404 页)。协作模型 = 好友关系 + 对象的collaboratorUserIds(购物清单/餐计划通用)。
6.5 导出 JSON-LD(schema.org)
curl -s -X POST http://127.0.0.1:8162/api/compat/v2/jobs/startExportJob \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"format":"jsonld","recipeIds":["fb231af7-5993-42d8-9bc8-e6a5862afaea"]}'
实测回显:{"jobId":"2e47aa87-0c7d-4b80-a75e-60fb78925c0a"}(⚠️ format 枚举 txt|pdf|jsonld)。轮询 job(参数名是 id 而非 jobId):
curl -s "http://127.0.0.1:8162/api/compat/v2/jobs/getJob?id=2e47aa87-0c7d-4b80-a75e-60fb78925c0a" \
-H "Authorization: Bearer $TOKEN"
实测回显:
{"id":"2e47aa87-0c7d-4b80-a75e-60fb78925c0a","status":"SUCCESS","type":"EXPORT","userId":"42754089-a1d9-4e1d-8b04-7df237fbb337","resultCode":0,"progress":100,"meta":{"exportType":"jsonld","exportScope":"recipeids","exportStorageBucket":"filesystem","exportStorageKey":"dataExport/2026/11/1793566536025-8296230e1b0ade","exportDownloadUrl":"api/images/filesystem/dataExport/2026/11/1793566536025-8296230e1b0ade","recipeIds":["fb231af7-5993-42d8-9bc8-e6a5862afaea"],"language":"en-us"},"createdAt":"2026-11-01T20:55:36.006Z","updatedAt":"2026-11-01T20:55:36.033Z"}
getJobDownloadUrlById 返回 {"signedUrl":"api/images/filesystem/dataExport/2026/11/1793566536025-8296230e1b0ade"}。⚠️ 实测经代理直接 GET 该相对路径返回 404(签名 URL 供前端带会话使用);产物实体可在 api 容器的 filesystem 存储卷内核验——存储键按年/月子目录展开(dataExport/2026/11/…),落盘真实路径即 /rs-media/ + 存储键:
docker exec recipesage_api sh -c 'wc -c "/rs-media/dataExport/2026/11/1793566536025-8296230e1b0ade" && head -c 300 "/rs-media/dataExport/2026/11/1793566536025-8296230e1b0ade"'
实测回显(输出节选,实测首尾:首行为 wc 结果,次行为产物前 300 字节原样):
778 /rs-media/dataExport/2026/11/1793566536025-8296230e1b0ade
{"recipes":[{"@context":"http://schema.org","@type":"Recipe","identifier":"fb231af7-5993-42d8-9bc8-e6a5862afaea","datePublished":"2026-11-01T20:54:59.373Z","description":"Garlic chili pasta","image":[],"name":"Pasta Aglio e Olio","prepTime":"PT10M","recipeIngredient":["200 g spaghetti","4 cloves gar
产物共 778 字节,为合法 schema.org JSON-LD(@type:Recipe、recipeIngredient 数组、recipeInstructions 为 HowToStep 列表——完整结构以 head -c 600 实测确认),导出闭环成立。
6.6 URL 抓取(clip)的 selfhost 限制
curl -s -X POST http://127.0.0.1:8162/api/clip -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"url":"https://www.example.com/recipe"}'
实测回显:{"status":400}(HTTP 400);带 html 字段同样 400。api 容器出站网络实测正常(容器内 wget https://example.com 成功),排除网络因素——v4 的 clip 走 AI 解析管道,未配 AI_API_KEY 时不可用。官方 README 给出的替代路径:用浏览器扩展(webextension-v3)直接抓取当前页 HTML,或配置 SCRAPFLY_API_KEY/AI key(参数见配置详解篇)。
6.7 数据库与备份
表结构规模:
docker exec recipesage_postgres psql -U recipesage_selfhost -d recipesage_selfhost \
-tAc "select count(*) from pg_tables where schemaname='public'"
实测回显:40(Users/Sessions/Recipes/Recipe_Images/Labels/LabelGroups/ShoppingLists/ShoppingListItems/MealPlans/Job/Friendships/_prisma_migrations 等)。
逻辑备份:
docker exec recipesage_postgres pg_dump -U recipesage_selfhost recipesage_selfhost | gzip > rs_backup.sql.gz
ls -la rs_backup.sql.gz
实测回显:-rw-r--r-- 1 root root 20890 Nov 2 04:56 rs_backup.sql.gz(约 20KB)。恢复可用 api 容器内 /app/scripts/restore-db.sh(dropdb → createdb → gunzip | psql),详见 CLI 篇。
七、数据持久化说明
docker volume ls | grep recipesage
实测回显:
local recipesage_apimedia
local recipesage_postgresdata
全部状态只存两处:postgresdata(PG 数据目录)与 apimedia(/rs-media:食谱图片、导出产物等,STORAGE_TYPE=filesystem 形态)。备份 = pg_dump + apimedia 卷打包,无第三处状态。
💡 技术拓展:Docker 数据持久化三种常见形态——
形态 特点 适用 named volume(本文) 由 Docker 管理于 /var/lib/docker/volumes/,权限自动处理,可随 compose 项目命名数据库/上传目录通用首选 bind mount 直接挂宿主目录,路径透明易备份,但受宿主权限/SELinux 影响 需要宿主侧直接读写配置或产物 tmpfs 仅内存,容器删除即失 敏感临时数据 官方 compose 用 named volume,备份走
pg_dump/卷打包;若你更习惯宿主目录直接管理,可把postgresdata:/var/lib/postgresql/data改写为 bind mount,数据等价。
八、环境变量表
selfhost 的权威配置面 = compose 顶部 x-common-env 锚点(api/pushpin/postgres 三方共享)。全量语义与改参闭环见同专栏《RecipeSage 4.1.1 配置参数详解》,此处给部署最小必知:
| 变量 | 默认值(compose) | 必改性 | 说明 |
|---|---|---|---|
API_PUBLIC_BASE_URL | 官方占位 https://example.com/api | 必改 | 对外基址,必须带 /api 后缀;本实测 http://127.0.0.1:8162/api |
GRIP_KEY | recipesage_selfhost | 可不改 | pushpin 签名密钥,仅 docker 内网使用 |
POSTGRES_DB/USER/PASSWORD | recipesage_selfhost | 建议改 | ⚠️ 三者改动必须同步改 DATABASE_URL |
DATABASE_URL | postgresql://…@postgres:5432/… | 联动改 | 格式 postgresql://USER:PASS@HOST:PORT/DB |
GROCERY_CATEGORIZER_URL | https://grocery-categorizer.recipesage.com/ | 可选 | 清单条目自动分类走公有 API;本地化见 compose 底部注释服务(需 AVX) |
AI_PROVIDER/AI_API_KEY/AI_API_BASE_URL | openrouter / 空 / 空 | 可选 | 配 key 才解锁 URL clip/OCR/PDF 解析 |
AI_MODEL_*(9 个) | gemini-2.5-flash(-lite)/claude-sonnet-4.6 | 可选 | openrouter 模型名格式 |
SCRAPFLY_API_KEY | 空 | 可选 | 绕 Cloudflare 反爬改善 URL 抓取 |
api 服务注入段(ENVIRONMENT、NODE_OPTIONS、STORAGE_TYPE、GRIP_URL、JOB_QUEUE_REDIS_HOST、STRIPE_* 置空等) | 见 4.2 全文 | 勿改 | 官方注释明确 "Please do not change",改动会破坏 selfhost 形态 |
九、配置文件挂载说明
RecipeSage selfhost 不读 .env、不挂载任何配置文件:配置全部内联在 compose(x-common-env + api environment 段),改配置 → docker compose -p recipesage up -d <service> 重建即生效(改参→重启→验证闭环实测见配置详解篇)。唯一"配置"例外是 nginx 路由——它固化在 proxy 镜像内(/etc/nginx/conf.d/default.conf),如需自定义路由,官方建议另挂自己的反代指向 proxy 容器,而不是改/删 proxy。
十、常用运维命令
# 查看栈状态
docker compose -p recipesage ps
# 跟踪 api 日志(排障主入口)
docker compose -p recipesage logs -f api
# 重启单个服务
docker compose -p recipesage restart api
# 升级:改 compose 中 api/static 的成对 tag 后
docker compose -p recipesage pull api static
docker compose -p recipesage up -d
# 停止整栈(数据卷保留)
docker compose -p recipesage down
升级要点:api 与 static 的 tag 必须成对改;迁移由 api 启动脚本自动执行(Prisma migrate deploy),无需手工动作;回滚 = tag 改回旧值重新 up(迁移向后兼容不保证,回滚前先 pg_dump)。
十一、常见问题(FAQ)
Q1: 首次 up -d 后访问 8162 全 502?
A: api 启动要先跑 69 个迁移 + seed + 全文检索回填,约需 30–60 秒。用 docker compose -p recipesage logs api 看到后端请求行出现后再访问;postgres 未 healthy 时 api 不会启动,ps 里确认 (healthy)。
Q2: 注册成功但前端功能异常/图片 404?
A: 九成是 API_PUBLIC_BASE_URL 没带 /api 后缀或端口与实际访问地址不一致。改 compose 后 docker compose -p recipesage up -d api 重建,用 curl http://<地址>/api/ 验证欢迎页。
Q3: createRecipe 一直 400?
A: 按 6.2 的清单逐字段补齐(全字段必填 + folder 只能 main/inbox)。400 响应体的 data 里带 Zod 逐项错误(invalid_type/invalid_value + path),照着补最快。
Q4: getRecipes GET 返回 404?
A: 它是 POST(参数在 body)。同理 getMyStats 是 GET、getJob 参数名为 id。以 /api/openapi.json(144 paths)为准。
Q5: 网页版"从 URL 导入"报 400?
A: selfhost 未配 AI_API_KEY 时 clip 不可用(见 6.6)。改用浏览器扩展抓取,或按 README 配 openrouter key。
Q6: 旧教程里的 /api/groups 404?
A: v4 已删除群组 API,协作改用好友 + collaboratorUserIds(见 6.4)。
Q7: Docker Hub 拉不到镜像?
A: 本实测环境即如此——经国内镜像加速源(如 docker.1ms.run/julianpoy/recipesage-selfhost:api-v4.1.1)拉取成功后 docker tag 回原名即可,注意注明网络渠道。
Q8: 导出 job 的 signedUrl 直接访问 404?
A: 属正常——该相对 URL 供前端带会话使用;产物在 apimedia 卷 /rs-media/dataExport/<年>/<月>/<键> 路径下(见 6.5)。
十二、参考链接
- 部署仓库(compose 权威来源):https://github.com/julianpoy/RecipeSage-selfhost
- 源码仓库:https://github.com/julianpoy/RecipeSage
- 官方文档:https://docs.recipesage.com (含 selfhost 与 AI key 配置指引)
- REST API 自描述:
http://<你的地址>/api/openapi.json - 同专栏进阶:配置参数全解见《RecipeSage 4.1.1 配置参数详解》,CLI 与运维工具见《RecipeSage 4.1.1 常用 CLI 命令与工具详解》
取证说明:本文全部命令与回显来自 2026-11-02 实测的 RecipeSage v4.1.1 六服务栈(Ubuntu 24.04.5 / Docker 29.9.2 / Compose v5.6.1,宿主端口 8162);镜像经国内镜像源拉取;浏览器工具不可用,页面验收降级为 HTTP+title+API 闭环。