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 RecipesFork Recipes自托管DockerPythonDjangoPostgreSQL2026/11/01

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 构建)
  • 端口规划(宿主侧):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:latestDocker 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-alpineDocker Hub424 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

三、前置条件

  1. Docker 20.10+ 与 Docker Compose v2+(实测 Docker 29.9.2 / Compose v5.6.1);注意 compose.sh 的版本检测 bug 只认 version v2 字样(见第十章);
  2. 磁盘预留 ≥ 8 GB(forkapi 镜像 2.96 GB + 前端镜像 726 MB + PostgreSQL 数据与媒体);
  3. 三个宿主端口空闲(8146/8148/5434,ss -ltn 确认,见第一章);
  4. git 可用(浅克隆 tag 源码);
  5. 宿主机 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 无宿主端口映射。本文做两处改动(其余保持原样):

  1. nginx 服务加 ports:"8146:80"(前端)、"8148:81"(API/管理面板);
  2. 新增 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_mediabe 与 nginx 的 /forkapi/media/食谱图片/视频/备份快照 zip✅ 实测 images/b3a050cf-..._tomato.png 在卷内
forkrecipes_web_staticbe 的 /forkapi/static/后端 collectstatic 产物✅ 启动自动生成
forkrecipes_web_static_feweb 的 /fork_recipes/static/前端静态(含 /static/src/output.css)✅
forkrecipes_uwsgi_data三方共享 /tmp/uwsgi/uwsgi.sock + uwsgi_recipes.sock运行期 socket,无需备份
forkrecipes_dataweb 的 /fork_recipes/sql/名义上前端 SQLite⚠️ 挂载错位 bug:前端实际库在 /fork_recipes/backend/sql/db.sqlite3(实测 135 KB 在容器层),卷内是空目录——容器删除即丢前端会话/用户镜像数据,修法见第十章与排错 FAQ 篇
bind tmp/fork-recipes/data/postgresdb 的 /var/lib/postgresql/data业务主数据(食谱/食材/步骤/用户)✅ 宿主目录,备份主体

业务数据以 PostgreSQL 为准;前端 SQLite 只存会话与用户信息镜像,官方错位挂载导致其不持久化,实际影响是「重建前端容器后所有人重新登录」,食谱数据不受影响。

六、环境变量表(.env 全键实测)

键实测值/示例必填说明
DJANGO_SECRETy5gyRi5f...(随机长串)✅前后端共用的 Django SECRET_KEY,随意生成
X_AUTH_HEADERced9106d-4f94-...(UUID)✅只读 API 自定义头的值;头名固定就叫 X-Auth-Header
SERVICE_BASE_URLhttp://192.168.1.172:8148✅前端调后端 API 的基址,必须宿主机可达(含协议+端口)
DEPLOYMENT_TYPEpostgres✅四选一:postgres/postgres-ssl/sqlite/sqlite-ssl,决定 compose 文件与后端 DB 解析
DATABASE_URLpostgres://forkuser:q1w2e3r4@127.0.0.1:5434/forkrecipespostgres 形态必填dj-database-url 解析;be 走 host 网络所以用 127.0.0.1:5434
SEED_DEFAULT_DATAtrue可选首启种子:管理员 admin@example.com/ChangeMe + 3 分类
CORS_ALLOWED_HOSTShttp://192.168.1.172,http://localhost建议前端访问 media 的允许主机(逗号分隔,至少两个)
PAGINATION_PAGE_SIZE20可选食谱搜索分页大小
EMAIL_HOST/USER/PASSWORD/PORT/USE_TLS留空可选仅「忘记密码」用;不配不影响登录与核心功能
OPENAI_API_KEY''(空)可选抓取/生成/翻译/TTS 四功能开关;官方模板要求带引号留空
OPENAI_MODELgpt-4o-mini可选LLM 抓取模型
OPEN_AI_TTS_MODEL_VOICEcoral可选朗读音色,10 选 1:alloy/ash/ballad/coral/echo/fable/onyx/nova/sage/shimmer

七、配置文件说明与挂载

  1. 迁移脚本:./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
  1. uwsgi 配置:两容器的 uwsgi.ini 都在镜像内(be 实测 socket=/tmp/uwsgi/uwsgi.sock,web 为 uwsgi_recipes.sock),经 uwsgi_data 共享卷被 nginx 反代;改 socket 路径需进容器改并重启。
  2. nginx 配置:nginx/nginx.conf 与 nginx/forkrecipes.nginx.conf 被 nginx/Dockerfile COPY 进镜像(不是运行时挂载)——改监听端口后必须 docker compose -f docker-compose-postgres.yml build nginx && up -d nginx 重建镜像才生效(本文 4.3 的改造即这样验证)。
  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 篇)

  1. ./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(实测可用)。
  2. sqlite 分支报 open .../docker-compose-sqlite.yaml: no such file or directory:仓库实际文件是 .yml 后缀(实测复现,官方脚本 bug)。
  3. 前端镜像构建失败 E: Package 'libpcre3-dev' has no installation candidate:python:3.12-slim 已切 trixie,改 FROM python:3.12-slim-bookworm(4.5 节实测修复)。
  4. API 全部 403 You must use authentication header:忘带 X-Auth-Header: <.env 的值> 头(头名带 -Header 后缀,实测踩坑)。
  5. 新建食谱 400:servings/prep_time/cook_time 必须整数;新谱必须带 image 文件字段;食材字段是 name/quantity、步骤字段是 text(4.7-④ 实测口径)。
  6. GET /api/recipe/ 返回 500:根 ViewSet 未实现 list 动作(官方 bug),列表用 GET /api/recipe/home/preview/?page=1(实测 200)。
  7. nginx 启动报 conflicting server name/两端口都 302 到同一处:忘了把第二个 server 块 listen 80 改 81(4.3 节)。
  8. 重建前端容器后要重新登录:data 卷挂载错位(/fork_recipes/sql/ vs 实际 /fork_recipes/backend/sql/),前端会话库不落卷(第五章实测回显)。

十一、参考链接

Fork Recipes Docker Compose 部署指南 - Quzuo