Fork Recipes Docker Compose 部署指南
Fork Recipes(fork.recipes v.4.1.1-Hot-fix + forkapi v.6.1.1)Docker Compose 部署实测:官方 postgres compose 自加 db 服务、nginx 双监听改造、bookworm 固定修构建、compose.sh 绕坑,前端登录/API 建谱/媒体/PG 落库端到端验收全部实测回显。
Fork Recipes Docker Compose 部署指南
目标:在单台 Linux 宿主机上用 Docker Compose 把 Fork Recipes(前端 mikebgrep/fork.recipes + 后端 mikebgrep/forkapi)完整拉起并通过端到端验收。项目定位、架构与选型结论见本专栏 《Fork Recipes 介绍与选型指南》;本文所有命令与回显均为本机 Ubuntu 24.04 实测(2026-11-01)。
部署口径(先读):Fork Recipes 官方提供 4 份 compose 文件(postgres / postgres-ssl / sqlite / sqlite-ssl)。本文采用
docker-compose-postgres.yml+ PostgreSQL 17 的生产形态——官方 postgres 文件假设数据库已在外部存在,本文自加db服务补全闭环;单机试用可改用docker-compose-sqlite.yml(后端零外部依赖,但官方compose.sh的 sqlite 分支有文件后缀 bug,见第十章)。官方 compose 未把任何端口映射到宿主机(be/web 走network_mode: host、nginx 仅容器内 80),本文按端口规划做了映射与 nginx 监听改造。
一、环境信息
- 宿主机:Ubuntu 24.04,IP
192.168.1.172,实测日期 2026-11-01 - 容器运行时实测版本:
$ docker --version
Docker version 29.9.2, build a82e528
$ docker compose version
Docker Compose version v5.6.1
- 组件版本口径:
- 前端 fork.recipes:git tag
v.4.1.1-Hot-fix(2025-04-28,commit 164e47d);该 tag 的requirements.txt钉 Django==5.1.8(master 分支已升 5.2.x,本文按 tag 为准) - 后端 forkapi:官方镜像
mikebgrep/forkapi:latest(对应 v.6.1.1.Hot-fix 系列,2025-04-28) - 数据库:
postgres:17-alpine(自加服务);前端自带小 SQLite(存会话/用户镜像) - 入口:
nginx:fork-recipes(仓库 nginx/Dockerfile 构建)
- 前端 fork.recipes:git tag
- 端口规划(宿主侧):8146 前端 Web UI、8148 API/管理面板、127.0.0.1:5434 PostgreSQL(仅本机)。8144/8145 已被同机其他项目占用,部署前逐一确认空闲:
$ ss -ltn | grep -E ':8146|:8148|:5434'
# 部署前无输出(空闲);部署后:
LISTEN 0 4096 0.0.0.0:8146 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:8148 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:5434 0.0.0.0:*
二、镜像选型说明
| 镜像 | 来源 | 大小(实测) | 说明 |
|---|---|---|---|
mikebgrep/forkapi:latest | Docker Hub 官方 | 2.96 GB | 后端全家桶(Django+DRF+jazzmin+Playwright 抓取依赖),README 明确镜像已含全部依赖;树莓派用 mikebgrep/forkapi:arm64 |
forkrecipes:latest | 本地构建(仓库根 Dockerfile) | 726 MB | 前端 Django 应用,官方不发行前端镜像 |
nginx:fork-recipes | 本地构建(仓库 nginx/Dockerfile) | 241 MB | 基于 nginx:latest,COPY 两份 conf 进镜像 |
postgres:17-alpine | Docker Hub | 424 MB | 官方 compose 不含数据库,本文自加 |
网络降级说明(实测发生):本机直连 Docker Hub 拉 mikebgrep/forkapi(2.96 GB)超时,改走国内镜像源 docker.1ms.run 拉取成功后重打官方标签,compose 文件无需改动:
$ docker pull docker.1ms.run/mikebgrep/forkapi:latest
Status: Image is up to date for docker.1ms.run/mikebgrep/forkapi:latest
docker.1ms.run/mikebgrep/forkapi:latest
$ docker tag docker.1ms.run/mikebgrep/forkapi:latest mikebgrep/forkapi:latest
$ docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' | grep forkapi
mikebgrep/forkapi:latest 2.96GB
docker.1ms.run/mikebgrep/forkapi:latest 2.96GB
三、前置条件
- Docker 20.10+ 与 Docker Compose v2+(实测 Docker 29.9.2 / Compose v5.6.1);注意 compose.sh 的版本检测 bug 只认
version v2字样(见第十章); - 磁盘预留 ≥ 8 GB(forkapi 镜像 2.96 GB + 前端镜像 726 MB + PostgreSQL 数据与媒体);
- 三个宿主端口空闲(8146/8148/5434,
ss -ltn确认,见第一章); - git 可用(浅克隆 tag 源码);
- 宿主机 IP 固定(
SERVICE_BASE_URL与CORS_ALLOWED_HOSTS都写它,换 IP 需同步改并重启)。
四、部署步骤
4.1 获取源码(按 tag 锁定版本)
$ git clone --branch v.4.1.1-Hot-fix --depth 1 https://github.com/mikebgrep/fork.recipes.git
$ cd fork.recipes && git log -1 --format='%h %ad' --date=short
164e47d 2025-04-28
$ ls docker-compose* .env.example compose.sh
.env.example compose.sh docker-compose-postgres.yml docker-compose-postgres-ssl.yml docker-compose-sqlite.yml docker-compose-sqlite-ssl.yml
仓库实际文件是
.yml后缀(不是官方脚本文档里写的.yaml);GitHub 直连慢时可先走代理192.168.1.72:7897或镜像站归档包,本文实测直连可用。
4.2 准备 .env
$ cp .env.example .env && vi .env
本文实测使用的完整 .env(键与官方模板一致,值为本机实值):
DJANGO_SECRET=y5gyRi5fjHKCIKANN0I6TzE6R65WpMfj7YXEa9oXET9SQC3LpV
X_AUTH_HEADER=ced9106d-4f94-4754-b20d-0bdbf49b4fb2
SERVICE_BASE_URL=http://192.168.1.172:8148
EMAIL_HOST=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_PORT=
EMAIL_USE_TLS=
DEPLOYMENT_TYPE=postgres
SEED_DEFAULT_DATA=true
DATABASE_URL=postgres://forkuser:q1w2e3r4@127.0.0.1:5434/forkrecipes
PAGINATION_PAGE_SIZE=20
CORS_ALLOWED_HOSTS=http://192.168.1.172,http://localhost
OPENAI_API_KEY=''
OPENAI_MODEL=gpt-4o-mini
OPEN_AI_TTS_MODEL_VOICE=coral
三个最容易接错的值:
SERVICE_BASE_URL必须写宿主机可达的 API 地址(含协议、带 8148 端口)。be/web 都是network_mode: host,前端拼的这个 URL 浏览器不访问、由前端进程自己回调,写成http://be:8000或容器名会直接连接失败;DATABASE_URL的 host:port 是从宿主机网络视角寻址数据库(be 用 host 网络),所以指向127.0.0.1:5434(db 服务的宿主映射),而不是db:5432;X_AUTH_HEADER是只读 API 的自定义头名(值任意固定串),后续所有 curl 验证都要带它。
4.3 nginx 监听改造(本地无域名部署的官方指引)
官方 nginx/forkrecipes.nginx.conf 的两个 server 块都写 listen 80(前端块 + API/管理面板块),同容器内两个 80 会冲突。官方文档 clients.md「Read for local deploy without domain name」一节明确要求本地部署把第二个 server 块改成别的端口。本文改 listen 80 → listen 81:
$ grep -n "listen" nginx/forkrecipes.nginx.conf
2: listen 80;
33: listen 81;
改后两块的分工:80 → uwsgi_recipes.sock(前端 UI),81 → uwsgi.sock(API + /admin 管理面板 + /media 图片直出)。
4.4 compose 改造:端口映射 + 自加 db 服务
官方 docker-compose-postgres.yml 假设 PostgreSQL 已在外部存在且 nginx 无宿主端口映射。本文做两处改动(其余保持原样):
nginx服务加 ports:"8146:80"(前端)、"8148:81"(API/管理面板);- 新增
db服务(postgres:17-alpine,数据 bind 到宿主tmp/fork-recipes/data/postgres,仅映射127.0.0.1:5434),并给be加depends_on: - db。
改后完整文件(实测在跑的版本):
services:
nginx:
build:
context: .
dockerfile: nginx/Dockerfile
image: nginx:fork-recipes
container_name: nginx
ports:
- "8146:80"
- "8148:81"
env_file:
- .env
command: /bin/sh -c "nginx -g 'daemon off;'"
volumes:
- uwsgi_data:/tmp/uwsgi/
- web_static:/forkapi/static/:ro
- web_media:/forkapi/media/
- web_static_fe:/fork_recipes/static/:ro
depends_on:
- web
- be
be:
image: mikebgrep/forkapi:latest # change with mikebgrep/forkapi:arm64 for RaspberryPI
container_name: forkapi
hostname: forkapi-host
env_file:
- .env
restart: always
network_mode: "host"
command: >
sh -c "chmod +x /scripts/migration.sh && /scripts/migration.sh ${SEED_DEFAULT_DATA} & uwsgi --ini uwsgi.ini --chown-socket=www-data:www-data"
volumes:
- ./scripts:/scripts
- uwsgi_data:/tmp/uwsgi/
- web_media:/forkapi/media/
- web_static:/forkapi/static/
depends_on:
- db
db:
image: postgres:17-alpine
container_name: fork-recipes-db
restart: always
environment:
POSTGRES_USER: forkuser
POSTGRES_PASSWORD: q1w2e3r4
POSTGRES_DB: forkrecipes
volumes:
- /opt/data/quzuo/tmp/fork-recipes/data/postgres:/var/lib/postgresql/data
ports:
- "127.0.0.1:5434:5432"
web:
build:
context: .
dockerfile: Dockerfile
image: forkrecipes:latest
container_name: recipes
env_file:
- .env
restart: always
network_mode: "host"
command: >
sh -c "uwsgi --ini uwsgi.ini --chown-socket=www-data:www-data"
volumes:
- data:/fork_recipes/sql/
- uwsgi_data:/tmp/uwsgi/
- web_static_fe:/fork_recipes/static/
depends_on:
- be
volumes:
data:
uwsgi_data:
web_static:
web_media:
web_static_fe:
4.5 修 Dockerfile 基础镜像(官方构建 bug,必须改)
前端仓库 Dockerfile 写 FROM python:3.12-slim,该标签现已解析到 Debian trixie,而官方 apt 列表里的 libpcre3-dev 在 trixie 已移除,直接构建失败(实测报错):
E: Package 'libpcre3-dev' has no installation candidate
Error: build failed: exit code: 100
修复:把基础镜像固定到 bookworm(在 Dockerfile 首行 FROM 处改):
$ grep -n "FROM" Dockerfile
4:FROM python:3.12-slim-bookworm
该坑与时间强相关:bookworm 阶段官方 Dockerfile 可正常构建,trixie 成为
slim默认后必失败。若你部署时python:3.12-slim已切回新发行版,报错形态相同、修法相同(固定一个仍含 libpcre3-dev 的 Debian 代号)。
4.6 构建并启动
不要用仓库自带的 compose.sh(两个 bug 见第十章),直接用 docker compose 指定 postgres 文件:
$ docker compose -f docker-compose-postgres.yml up -d
[+] Building 2/4: nginx:fork-recipes, forkrecipes:latest ...
✔ Image forkrecipes:latest Built
✔ Container fork-recipes-db Started
✔ Container forkapi Started
✔ Container recipes Started
✔ Container nginx Started
启动后状态(实测):
$ docker compose -f docker-compose-postgres.yml ps --format '{{.Name}}\t{{.Service}}\t{{.State}}'
fork-recipes-db db running
forkapi be running
nginx nginx running
recipes web running
后端首次启动会自动跑迁移与种子数据(scripts/migration.sh + SEED_DEFAULT_DATA=true),docker logs forkapi 关键回显(输出节选,实测首尾):
Migrations for 'recipe':
recipe/migrations/0001_initial.py
Operations to perform:
Apply all migrations: admin, auth, authentication, authtoken, backupper, contenttypes, recipe, schedule, sessions, shopping
Running migrations:
Applying admin.0001_initial... OK
...
Admin user created: admin@example.com
种子数据同时写入 3 个演示分类(Greek 🌮 / Italian 🍝 / Mexican 🌶️),管理员账号 admin@example.com / ChangeMe(首次登录后应改密)。
4.7 端到端验收
① 前端登录链路(未登录跳登录页 → CSRF → 登录 302 回首页 → 首页渲染食谱):
$ curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" http://192.168.1.172:8146/
302 -> http://192.168.1.172:8146/login/?next=/
$ curl -s -c cookies.txt -o /dev/null -w "%{http_code}\n" http://192.168.1.172:8146/login/
200
$ curl -s -b cookies.txt -c cookies.txt -o /dev/null -w "%{http_code} -> %{redirect_url}\n" \
-X POST http://192.168.1.172:8146/login/ \
-d "csrfmiddlewaretoken=<cookies.txt中的csrftoken>&email=admin@example.com&password=ChangeMe" \
-e http://192.168.1.172:8146/login/
302 -> http://192.168.1.172:8146/
$ curl -s -b cookies.txt http://192.168.1.172:8146/ -w "HTTP:%{http_code}\n" -o home.html
HTTP:200
$ grep -c "西红柿炒蛋" home.html # 首页已渲染实测创建的食谱
1
② API 换 token(必须带 X-Auth-Header,头名与值都来自 .env):
$ curl -s -X POST http://192.168.1.172:8148/api/auth/token \
-H "Content-Type: application/json" -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-d '{"email":"admin@example.com","password":"ChangeMe"}'
{"token":"59cfa1b255ba57535043e4b80fde7eeae393ce0d","user":{"username":"admin","email":"admin@example.com","is_superuser":true}}
不带 X-Auth-Header 时该端点直接 403 {"detail":"You must use authentication header"}(实测,见第十章)。
③ 只读端点验证(X-Auth-Header 免 token 读分类):
$ curl -s http://192.168.1.172:8148/api/recipe/category -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2"
[{"pk":1,"name":"Greek 🌮"},{"pk":2,"name":"Italian 🍝"},{"pk":3,"name":"Mexican 🌶️"},{"pk":4,"name":"中式"}]
(中式 为验收时经 POST api/recipe/category/add 创建,回显 {"pk":4,"name":"中式"}。)
④ 建谱全链路(multipart 建谱 → 食材 → 步骤 → 关联分类)。必填字段实测口径:name/description/servings/prep_time/cook_time 缺一不可,servings/prep_time/cook_time 必须是整数,新建食谱必须带 image 文件(否则 400 "Image is required for new recipes."):
$ curl -s -X POST http://192.168.1.172:8148/api/recipe/ \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-F "name=西红柿炒蛋" -F "description=国民下饭菜" -F "servings=2" \
-F "prep_time=5" -F "cook_time=10" -F "image=@tomato.png"
{"pk":1,"image":"http://192.168.1.172:8148/media/images/b3a050cf-911d-46de-b8fc-6faa8badc9ea_tomato.png","name":"西红柿炒蛋","servings":2,"chef":"admin","description":"国民下饭菜","category":null,"prep_time":5,"cook_time":10,"ingredients":[],"steps":[],"language":"English","is_original":true}
(输出节选:完整响应含 video/tag/total_time/difficulty/is_favorite/reference/is_translated/audio_instructions 等全部字段,实测共 24 键)
HTTP:201
$ curl -s -X POST http://192.168.1.172:8148/api/recipe/1/ingredients \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-d '[{"name":"西红柿","quantity":"2个"},{"name":"鸡蛋","quantity":"3个"}]' # 字段是 name/quantity
$ curl -s -X POST http://192.168.1.172:8148/api/recipe/1/steps \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-d '[{"text":"鸡蛋加盐打散炒至凝固盛出"},{"text":"西红柿炒出汁水回锅翻匀"}]' # 字段是 text
$ curl -s -X PATCH http://192.168.1.172:8148/api/recipe/1/category \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" \
-H "Content-Type: application/json" -d '{"category_pk":4}'
读回验证(分页列表走 home/preview,注意根路径 GET api/recipe/ 会 500,见第十章):
$ curl -s "http://192.168.1.172:8148/api/recipe/home/preview/?page=1" -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2"
{"count":1,"next":null,"previous":null,"results":[{"pk":1,"image":"http://192.168.1.172:8148/media/images/b3a050cf-911d-46de-b8fc-6faa8badc9ea_tomato.png","name":"西红柿炒蛋","chef":"admin","servings":2,"total_time":"0.15","difficulty":null,"is_favorite":false}]}
$ curl -s "http://192.168.1.172:8148/api/recipe/1/" -H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" | head -c 200
{"pk":1,"image":"http://192.168.1.172:8148/media/images/b3a050cf-911d-46de-b8fc-6faa8badc9ea_tomato.png","name":"西红柿炒蛋","servings":2,"chef":"admin","video":null,"description":"国民下饭菜","category":{"pk":4,"name":"中式"},"tag":null,"prep_time":5,"cook_time":10,
(输出节选(实测首尾):head -c 200 截断,完整详情另含 total_time/ingredients/steps 等字段)
删除验证(204,列表回到 count=1 不受影响):
$ curl -s -X DELETE http://192.168.1.172:8148/api/recipe/2/ \
-H "Authorization: Token 59cfa1b255ba57535043e4b80fde7eeae393ce0d" \
-H "X-Auth-Header: ced9106d-4f94-4754-b20d-0bdbf49b4fb2" -w "HTTP:%{http_code}\n"
HTTP:204
⑤ 媒体与管理面板:
$ curl -s -o /dev/null -w "HTTP:%{http_code} size:%{size_download}\n" http://127.0.0.1:8148/media/images/b3a050cf-911d-46de-b8fc-6faa8badc9ea_tomato.png
HTTP:200 size:70
$ curl -s -o /dev/null -w "HTTP:%{http_code} -> %{redirect_url}\n" http://192.168.1.172:8148/admin/
302 -> http://192.168.1.172:8148/login/?next=/admin/
(管理面板登录入口在 8148 端口的 /login/,账号同 admin@example.com/ChangeMe;前端 UI 无注册入口,新增用户要在管理面板操作——官方 clients.md 口径。)
⑥ 数据库落库核对:
$ docker exec fork-recipes-db psql -U forkuser -d forkrecipes -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)
五、数据持久化说明
compose 项目名取目录名规范化(fork.recipes → forkrecipes),5 个命名卷实测:
$ docker volume ls | grep forkrecipes
local forkrecipes_data
local forkrecipes_uwsgi_data
local forkrecipes_web_media
local forkrecipes_web_static
local forkrecipes_web_static_fe
| 卷/路径 | 挂载点 | 内容 | 持久化结论 |
|---|---|---|---|
forkrecipes_web_media | be 与 nginx 的 /forkapi/media/ | 食谱图片/视频/备份快照 zip | ✅ 实测 images/b3a050cf-..._tomato.png 在卷内 |
forkrecipes_web_static | be 的 /forkapi/static/ | 后端 collectstatic 产物 | ✅ 启动自动生成 |
forkrecipes_web_static_fe | web 的 /fork_recipes/static/ | 前端静态(含 /static/src/output.css) | ✅ |
forkrecipes_uwsgi_data | 三方共享 /tmp/uwsgi/ | uwsgi.sock + uwsgi_recipes.sock | 运行期 socket,无需备份 |
forkrecipes_data | web 的 /fork_recipes/sql/ | 名义上前端 SQLite | ⚠️ 挂载错位 bug:前端实际库在 /fork_recipes/backend/sql/db.sqlite3(实测 135 KB 在容器层),卷内是空目录——容器删除即丢前端会话/用户镜像数据,修法见第十章与排错 FAQ 篇 |
bind tmp/fork-recipes/data/postgres | db 的 /var/lib/postgresql/data | 业务主数据(食谱/食材/步骤/用户) | ✅ 宿主目录,备份主体 |
业务数据以 PostgreSQL 为准;前端 SQLite 只存会话与用户信息镜像,官方错位挂载导致其不持久化,实际影响是「重建前端容器后所有人重新登录」,食谱数据不受影响。
六、环境变量表(.env 全键实测)
| 键 | 实测值/示例 | 必填 | 说明 |
|---|---|---|---|
DJANGO_SECRET | y5gyRi5f...(随机长串) | ✅ | 前后端共用的 Django SECRET_KEY,随意生成 |
X_AUTH_HEADER | ced9106d-4f94-...(UUID) | ✅ | 只读 API 自定义头的值;头名固定就叫 X-Auth-Header |
SERVICE_BASE_URL | http://192.168.1.172:8148 | ✅ | 前端调后端 API 的基址,必须宿主机可达(含协议+端口) |
DEPLOYMENT_TYPE | postgres | ✅ | 四选一:postgres/postgres-ssl/sqlite/sqlite-ssl,决定 compose 文件与后端 DB 解析 |
DATABASE_URL | postgres://forkuser:q1w2e3r4@127.0.0.1:5434/forkrecipes | postgres 形态必填 | dj-database-url 解析;be 走 host 网络所以用 127.0.0.1:5434 |
SEED_DEFAULT_DATA | true | 可选 | 首启种子:管理员 admin@example.com/ChangeMe + 3 分类 |
CORS_ALLOWED_HOSTS | http://192.168.1.172,http://localhost | 建议 | 前端访问 media 的允许主机(逗号分隔,至少两个) |
PAGINATION_PAGE_SIZE | 20 | 可选 | 食谱搜索分页大小 |
EMAIL_HOST/USER/PASSWORD/PORT/USE_TLS | 留空 | 可选 | 仅「忘记密码」用;不配不影响登录与核心功能 |
OPENAI_API_KEY | ''(空) | 可选 | 抓取/生成/翻译/TTS 四功能开关;官方模板要求带引号留空 |
OPENAI_MODEL | gpt-4o-mini | 可选 | LLM 抓取模型 |
OPEN_AI_TTS_MODEL_VOICE | coral | 可选 | 朗读音色,10 选 1:alloy/ash/ballad/coral/echo/fable/onyx/nova/sage/shimmer |
七、配置文件说明与挂载
- 迁移脚本:
./scripts:/scripts只读挂进 be,启动命令先跑migration.sh(makemigrations ×5 app → migrate → collectstatic → 按参数 seed)。脚本原文(实测在跑):
python manage.py makemigrations authentication
python manage.py makemigrations recipe
python manage.py makemigrations schedule
python manage.py makemigrations shopping
python manage.py makemigrations backupper
python manage.py migrate
python manage.py collectstatic --noinput
if [ "$1" == 'true' ]; then
python manage.py seed_admin_user
python manage.py seed_categories
fi
- uwsgi 配置:两容器的
uwsgi.ini都在镜像内(be 实测socket=/tmp/uwsgi/uwsgi.sock,web 为uwsgi_recipes.sock),经uwsgi_data共享卷被 nginx 反代;改 socket 路径需进容器改并重启。 - nginx 配置:
nginx/nginx.conf与nginx/forkrecipes.nginx.conf被nginx/DockerfileCOPY 进镜像(不是运行时挂载)——改监听端口后必须docker compose -f docker-compose-postgres.yml build nginx && up -d nginx重建镜像才生效(本文 4.3 的改造即这样验证)。 .env:nginx/be/web 三服务env_file: .env注入;db 服务用自身environment块(不在官方 env 体系内,本文自加)。
八、等价 docker run
按本文 compose 展开的等价单命令形态(供不用 compose 的场景参考;卷名沿用 compose 已建的 forkrecipes_* 即可保留数据):
# 数据库
docker run -d --name fork-recipes-db --restart always \
-e POSTGRES_USER=forkuser -e POSTGRES_PASSWORD=q1w2e3r4 -e POSTGRES_DB=forkrecipes \
-v /opt/data/quzuo/tmp/fork-recipes/data/postgres:/var/lib/postgresql/data \
-p 127.0.0.1:5434:5432 postgres:17-alpine
# 后端(host 网络,启动自带迁移)
docker run -d --name forkapi --restart always --network host \
--env-file .env \
-v $(pwd)/scripts:/scripts -v forkrecipes_uwsgi_data:/tmp/uwsgi/ \
-v forkrecipes_web_media:/forkapi/media/ -v forkrecipes_web_static:/forkapi/static/ \
mikebgrep/forkapi:latest \
sh -c "chmod +x /scripts/migration.sh && /scripts/migration.sh ${SEED_DEFAULT_DATA} & uwsgi --ini uwsgi.ini --chown-socket=www-data:www-data"
# 前端(镜像需先 docker compose build web 产出)
docker run -d --name recipes --restart always --network host \
--env-file .env \
-v forkrecipes_data:/fork_recipes/sql/ -v forkrecipes_uwsgi_data:/tmp/uwsgi/ \
-v forkrecipes_web_static_fe:/fork_recipes/static/ \
forkrecipes:latest \
sh -c "uwsgi --ini uwsgi.ini --chown-socket=www-data:www-data"
# 入口(镜像需先 build nginx)
docker run -d --name nginx --restart always \
-p 8146:80 -p 8148:81 --env-file .env \
-v forkrecipes_uwsgi_data:/tmp/uwsgi/ -v forkrecipes_web_static:/forkapi/static/:ro \
-v forkrecipes_web_media:/forkapi/media/ -v forkrecipes_web_static_fe:/fork_recipes/static/:ro \
nginx:fork-recipes
九、常用运维命令
$ docker compose -f docker-compose-postgres.yml ps # 服务状态
$ docker compose -f docker-compose-postgres.yml restart web # 前端配置改动后重启(实测回显)
Container recipes Restarting
Container recipes Started
$ docker logs -f forkapi # 后端日志(迁移/API 访问行)
$ docker logs -f recipes # 前端日志
$ docker compose -f docker-compose-postgres.yml down # 停止并删容器(卷与 PG 数据保留)
$ docker compose -f docker-compose-postgres.yml up -d # 再拉起(幂等,迁移自动跳过已应用)
日常巡检、备份恢复与升级回滚的完整操作流见本专栏《Fork Recipes 日常运维与升级手册》。
十、常见问题(速查,详解见排错 FAQ 篇)
./compose.sh up报docker-compose: command not found:脚本版本检测只匹配version v2,Compose v5 输出Docker Compose version v5.6.1匹配失败回退老命令。绕过:直接docker compose -f docker-compose-postgres.yml up -d(实测可用)。- sqlite 分支报
open .../docker-compose-sqlite.yaml: no such file or directory:仓库实际文件是.yml后缀(实测复现,官方脚本 bug)。 - 前端镜像构建失败
E: Package 'libpcre3-dev' has no installation candidate:python:3.12-slim已切 trixie,改FROM python:3.12-slim-bookworm(4.5 节实测修复)。 - API 全部 403
You must use authentication header:忘带X-Auth-Header: <.env 的值>头(头名带 -Header 后缀,实测踩坑)。 - 新建食谱 400:
servings/prep_time/cook_time必须整数;新谱必须带image文件字段;食材字段是name/quantity、步骤字段是text(4.7-④ 实测口径)。 GET /api/recipe/返回 500:根 ViewSet 未实现 list 动作(官方 bug),列表用GET /api/recipe/home/preview/?page=1(实测 200)。- nginx 启动报
conflicting server name/两端口都 302 到同一处:忘了把第二个 server 块listen 80改81(4.3 节)。 - 重建前端容器后要重新登录:
data卷挂载错位(/fork_recipes/sql/vs 实际/fork_recipes/backend/sql/),前端会话库不落卷(第五章实测回显)。
十一、参考链接
- 前端仓库(含 compose 与 Dockerfile):https://github.com/mikebgrep/fork.recipes
- 后端仓库:https://github.com/mikebgrep/forkapi
- 官方文档站:https://mikebgrep.github.io/forkapi/latest/
- 官方 Docker Hub:https://hub.docker.com/r/mikebgrep/forkapi
- 本专栏:《Fork Recipes 介绍与选型指南》(架构与选型)、《Fork Recipes 日常运维与升级手册》(备份/升级)、《Fork Recipes 故障排查与 FAQ》(坑位详解)