Our Shopping List Docker Compose 部署指南

用官方镜像与 Docker Compose 三服务栈(app + FerretDB + PostgreSQL)部署 Our Shopping List 5.0.0,覆盖部署、持久化、备份、升级与卸载全流程,全部步骤实测通过。

Our Shopping List自托管2026/10/24

Our Shopping List Docker Compose 部署指南

目标:在一台 Linux 服务器上用官方镜像 + Docker Compose 部署一套可用的 Our Shopping List(OSL)5.0.0 服务(应用 + FerretDB + PostgreSQL 三服务栈),覆盖部署、数据持久化、备份、升级与卸载。全文步骤已在 Linux x86_64 + Docker 28.3.3 实测通过(2026-10-24)。

一、环境信息

  • 操作系统:Linux x86_64(内核 6.8)
  • Docker:28.3.3(含 compose v2 子命令)
  • 部署版本:ourshoppinglist/our-shopping-list:5.0.0(应用)、ghcr.io/ferretdb/ferretdb:2.7.0(协议兼容层)、ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0(存储)
  • 监听端口:宿主机 8132 → 应用服务 8080(8080 为镜像内默认 LISTEN_PORT)
  • Compose 项目名:ourshoppinglist-5-0-0-stack(版本号中的点不能用于项目名,故写作 5-0-0)
  • 数据卷:ourshoppinglist-5-0-0-stack_pgdbdata(PostgreSQL 数据)
  • 部署目录:/opt/osl(本文示例路径,可自行替换)

二、镜像选型说明

OSL 官方只发布一个应用镜像,数据库层由配套镜像组成,先弄清每个镜像的角色再动手:

镜像角色实测体积(本地 docker images)说明
ourshoppinglist/our-shopping-list:5.0.0应用(Node.js 服务端 + 预构建前端)351 MB(解压后;Docker Hub 压缩包 79 MB)Docker Hub Sponsored OSS;latest 与 5.0.0 同步发布
ghcr.io/ferretdb/ferretdb:2.7.0MongoDB 协议 → PostgreSQL 翻译层53.6 MB5.0.0 起官方正式支持的数据库栈
ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0实际存储(PostgreSQL 17 + documentdb 扩展)2.31 GB版本号必须与 FerretDB 版本配套
mongo:4.x~7.x遗留数据库方案—旧部署沿用;Mongo→FerretDB 官方有迁移文档

选型结论:

  1. 应用镜像固定锁版本号(如 :5.0.0),不要用 latest 管理生产,升级时显式改 tag。
  2. 新部署一律选 FerretDB + PostgreSQL 栈:免 MongoDB SSPL 许可顾虑、备份回归 PostgreSQL 生态;官方仓库 doc/docker-compose.ferretdb.yml 即此栈。
  3. FerretDB 与 postgres-documentdb 的版本是配套关系(postgres 镜像 tag 尾缀 ferretdb-2.7.0 标明),升级 FerretDB 必须同步换存储镜像 tag。
  4. 已有 MongoDB 运维体系且不想动的,可用官方遗留编排 doc/docker-compose.mongodb.yml,行为等价(MongoDB 认证暂不支持,见官方 README)。

三、前置条件

  • 一台 Linux 服务器,已安装 Docker 24+ 与 compose v2(docker compose version 能出版本号即可)。
  • 一个空闲的宿主机端口(本文用 8132;规划端口前用 ss -ltn | grep :<端口> 确认未被占用,避免与宿主机上其他服务冲突——多服务共存的服务器上端口被抢是最常见的第一坑)。
  • 服务器与使用者在同一可达网络(局域网或 VPN):OSL 无内置认证,不要直接暴露公网;公网场景必须套反向代理 + 认证(见《Our Shopping List 5.0.0 介绍与选型指南》选型限制一节)。
  • 磁盘:应用+依赖镜像约 2.7 GB(主要是 PostgreSQL 配套镜像),数据增长极慢(纯文本条目),10 GB 余量绰绰有余。

四、部署步骤

4.1 准备部署目录与编排文件

创建 /opt/osl 目录,包含两个文件:compose.yaml(编排)与 .env(数据库账号)。

compose.yaml 全文(在官方 doc/docker-compose.ferretdb.yml 基础上:显式项目名、锁版本、加映射端口与健康检查依赖):

name: ourshoppinglist-5-0-0-stack

services:
  app:
    image: ourshoppinglist/our-shopping-list:5.0.0
    restart: unless-stopped
    ports:
      - '8132:8080'
    environment:
      MONGODB_HOST: ferretdb
      VITE_APP_I18N_LOCALE: zh
      VITE_APP_I18N_FALLBACK_LOCALE: en
      VITE_APP_I18N_FORCE_LOCALE: 0
      VITE_APP_SINGLEBOARD_MODE: 0
    depends_on:
      - ferretdb

  ferretdb:
    image: ghcr.io/ferretdb/ferretdb:2.7.0
    restart: unless-stopped
    environment:
      FERRETDB_AUTH: false
      FERRETDB_POSTGRESQL_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/postgres"
    depends_on:
      - postgres

  postgres:
    image: ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: postgres
    volumes:
      - pgdbdata:/var/lib/postgresql/data

volumes:
  pgdbdata:

networks:
  default:
    name: osl-5-0-0-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.61.0/24

.env 全文(演示口令,仅限内网练习环境;生产请换成强口令并用 chmod 600 .env 收权):

POSTGRES_USER=osl
POSTGRES_PASSWORD=q1w2e3r4

💡 说明:不给 .env 写 POSTGRES_DB——它会被 environment 段的 POSTGRES_DB: postgres 覆盖,写了反而误导备份时的库名判断(数据库名以 FERRETDB_POSTGRESQL_URL 指向的 postgres 为准,见 5.2 节实测)。

逐项说明:

  • name: ourshoppinglist-5-0-0-stack:显式 compose 项目名,决定资源前缀(网络/卷/服务名);版本号里的点不合法,所以写成 5-0-0。显式命名可避免「换目录重新 up 后项目名变化导致卷对不上」的经典事故。
  • MONGODB_HOST: ferretdb:应用按 MongoDB 协议连这个主机名(compose 网络内 DNS);端口/库名默认 27017/osl,无需显式。
  • FERRETDB_AUTH: false:FerretDB 关闭认证(OSL 的 mongoose 暂不支持带认证连接,官方默认即此形态);数据库层隔离靠 compose 内部网络。
  • FERRETDB_POSTGRESQL_URL:引用 .env 里的账号口令,避免口令散落两处。
  • pgdbdata 具名卷挂到 PostgreSQL 数据目录:这是全栈唯一有状态点,应用与 FerretDB 均无状态,可随时重建。
  • networks 段:把默认网络显式命名并固定网段(本文实测用 10.89.61.0/24 空闲槽)。多套 compose 栈共存的宿主机上,显式子网可避免与相邻栈的网段冲突;单栈环境删掉整个 networks 段、用项目默认网络也完全可以。
  • 端口映射 '8132:8080':左侧换成你规划的端口;右侧 8080 是镜像默认监听口,除非改了 LISTEN_PORT 否则不动。

4.2 拉取镜像并启动

cd /opt/osl
docker compose up -d

实测回显(输出节选,2026-10-24 实抓):

 Network osl-5-0-0-net Created
 Container ourshoppinglist-5-0-0-stack-postgres-1 Created
 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Created
 Container ourshoppinglist-5-0-0-stack-app-1       Created
 Container ourshoppinglist-5-0-0-stack-postgres-1  Started
 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Started
 Container ourshoppinglist-5-0-0-stack-app-1       Started

(首次部署还会多一条 Volume "ourshoppinglist-5-0-0-stack_pgdbdata" Created;上表取自复用既有数据卷的重建周期,2026-10-24 实抓。)

4.3 验证部署

先看三个服务的运行与健康状态:

docker compose ps

实测回显(节选,Status 列随时间会从 health: starting 变为 healthy):

NAME                                    STATUS
ourshoppinglist-5-0-0-stack-app-1       Up 2 minutes (healthy)
ourshoppinglist-5-0-0-stack-ferretdb-1  Up 2 minutes (healthy)
ourshoppinglist-5-0-0-stack-postgres-1  Up 2 minutes

再打应用健康检查端点:

curl http://127.0.0.1:8132/healthcheck

实测回显:

{"success":true}

最后看一眼应用启动日志,确认版本与数据库连接:

docker compose logs app --tail 40

实测回显(输出节选,过滤了堆栈行):

APP_VERSION: '5.0.0'
APP_BUILD_ID: '1d6f81d3'
APP_ENV: 'production'
NODE_VERSION: 22.23.4
Serving socket-io on /socket.io/
OSL Server started on [::]:8080

⚠️ 注意:日志里同时会出现一段 client/dist/manifest.json 的 ENOENT 报错栈(PWA 中间件找不到 manifest 文件)。实测不影响任何功能,属于 5.0.0 镜像内的已知噪音,无需处理。

浏览器打开 http://<服务器IP>:8132/ 应能看到 OSL 首页(首页空面板属正常,首次访问面板 slug 时会自动创建面板)。

五、数据持久化说明

5.1 有状态点与数据归属

数据位置说明
面板/清单/条目PostgreSQL 具名卷 ourshoppinglist-5-0-0-stack_pgdbdata全部业务数据
应用配置compose.yaml 的 environment无状态,文件即配置
用户浏览器侧localStorage(键前缀 OurShoppingList_)PWA 本地缓存,清了会重新拉取

持久化实测:本文实验栈经历多次 docker compose down + up -d 循环(期间还单独重建过应用服务),family 面板与清单条目数据始终完好——业务数据全部落在 pgdbdata 卷,重启/重建不丢。

5.2 备份

业务数据在 PostgreSQL 里,标准姿势是 pg_dump。注意库名是 postgres:FerretDB 的连接串(FERRETDB_POSTGRESQL_URL)指向 postgres 库,OSL 数据就存在里面;.env 里给存储服务声明的 POSTGRES_DB 会被 compose environment 段的 POSTGRES_DB: postgres 覆盖,用错库名会得到 FATAL: database "osl" does not exist(实测踩过)。

docker compose exec postgres pg_dump -U osl -d postgres -F c -f /tmp/osl-$(date +%F).dump
docker compose cp postgres:/tmp/osl-2026-10-24.dump /opt/osl/backup/

实测回显(2026-10-24 实抓;pg_dump 正常退出无输出):

ourshoppinglist-5-0-0-stack-postgres-1 Copying ourshoppinglist-5-0-0-stack-postgres-1:/tmp/osl-2026-10-24.dump to /opt/osl/backup/
ourshoppinglist-5-0-0-stack-postgres-1 Copied ourshoppinglist-5-0-0-stack-postgres-1:/tmp/osl-2026-10-24.dump to /opt/osl/backup/

-rw-r--r-- 1 root root 72032 Oct 24 10:39 osl-2026-10-24.dump

5.3 恢复

docker compose cp /opt/osl/backup/osl-2026-10-24.dump postgres:/tmp/
docker compose exec postgres pg_restore -U osl -d postgres --clean /tmp/osl-2026-10-24.dump

💡 说明:pg_dump -F c 是自定义压缩格式,pg_restore --clean 会先删后建,适合整库覆盖恢复。恢复前先 docker compose stop app 停写,恢复完成后再启动。

六、环境变量表

应用服务(app)的完整环境变量面(系统键 + 应用键,与官方 README 5.0.0 一致,逐项实测详见《Our Shopping List 5.0.0 配置参数详解》):

系统键:

变量默认值说明
LISTEN_PORT8080应用监听端口(改它时同步改 ports 映射右侧)
MONGODB_HOSTmongodb数据库主机名,FerretDB 栈下填 ferretdb
MONGODB_PORT27017数据库端口
MONGODB_DBosl数据库名
BASE_URL空子路径部署时设(如 /my-osl/),见 8.2 节

应用键(VITE_APP_*,摘要——运行时经 /config.js 注入前端):

变量默认值说明
VITE_APP_I18N_LOCALEen界面语言,zh 为简体中文
VITE_APP_I18N_FALLBACK_LOCALEen回退语言
VITE_APP_I18N_FORCE_LOCALE01 = 强制忽略浏览器语言
VITE_APP_SINGLEBOARD_MODE01 = 单面板模式(v1 行为)
VITE_APP_BOARD_DELETION_ENABLED01 = 允许删除面板
VITE_APP_LIST_ALL_BOARDS_ENABLED01 = 允许列出全部面板
VITE_APP_TITLE / VITE_APP_SHORT_TITLEOur Shopping List / OSL站点标题/PWA 短标题
VITE_APP_HOME_MESSAGE空首页提示语
VITE_APP_THEMEsystemsystem/light/dark
VITE_APP_LOCALSTORAGE_KEY_PREFIXOurShoppingList_浏览器存储键前缀
VITE_APP_SOCKETIO_PING_INTERVAL / _TIMEOUT / _CSR_MAXDISCONNECTIONDURATION25000 / 20000 / 1800000实时通道心跳与断线恢复窗口(毫秒)

数据库服务(postgres)消费 .env 的 POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB;ferretdb 消费 FERRETDB_AUTH 与 FERRETDB_POSTGRESQL_URL。

七、配置参数说明

  • 改配置 = 改 compose.yaml 的 environment + 重启:VITE_APP_* 是服务端启动时读取并经 /config.js 下发的(实测验证:改 VITE_APP_TITLE 重启后 curl /config.js 立即出新值),无需重新构建。
  • 重启生效命令:
docker compose up -d app

实测回显(改 VITE_APP_TITLE 后执行 docker compose up -d app,2026-10-24 实抓):

 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Running
 Container ourshoppinglist-5-0-0-stack-postgres-1  Running
 Container ourshoppinglist-5-0-0-stack-app-1       Recreate
 Container ourshoppinglist-5-0-0-stack-app-1       Recreated
 Container ourshoppinglist-5-0-0-stack-app-1       Starting
 Container ourshoppinglist-5-0-0-stack-app-1       Started
  • 验证配置是否生效的两个抓手:curl http://127.0.0.1:8132/config.js(前端实际拿到的配置)与 docker compose exec app node cli.js config:show(服务端读取的完整配置)。所有参数的语义、默认值与实测行为逐项见《Our Shopping List 5.0.0 配置参数详解》。

八、配置文件挂载

8.1 robots.txt(默认全站 Disallow,可替换)

镜像内置 robots.txt 拦截所有搜索引擎(实测默认内容 User-agent: * + Disallow: /)。要放开/自定义,把文件只读挂到 /app/robots.txt:

    volumes:
      - ./robots.txt:/app/robots.txt:ro

实测验证(挂载自定义文件后):

$ curl http://127.0.0.1:8132/robots.txt
User-agent: *
Disallow: /

8.2 子路径部署(BASE_URL)

需要挂在反向代理的子路径下(如 https://host.example/osl/)时,给应用服务加 BASE_URL: osl2/(必须带尾部斜杠)。实测行为:

  • 根路径直接访问返回 400 Bad Request(应用拒绝非子路径访问);
  • 子路径 /osl2/ 正常出页面,/osl2/config.js 中 BASE_URL 为 "osl2/",静态资源以相对路径 ./assets/... 加载;
  • 同一台服务器跑多实例不同子路径时,各实例必须设置不同的 VITE_APP_LOCALSTORAGE_KEY_PREFIX(否则浏览器端两实例互相覆盖本地数据),这是官方 README 明示的要求。

九、等价 docker run

不想用 compose 时,等价的手工拉起顺序如下(先建网络,再依次起 postgres → ferretdb → app):

# 1. 建独立网络
docker network create osl-5-0-0-net

# 2. 存储层:PostgreSQL 17(documentdb)
docker run -d --name osl-postgres \
  --network osl-5-0-0-net \
  -e POSTGRES_USER=osl -e POSTGRES_PASSWORD=q1w2e3r4 -e POSTGRES_DB=postgres \
  -v osl_pgdbdata:/var/lib/postgresql/data \
  --restart unless-stopped \
  ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0

# 3. 协议层:FerretDB 2.7(无认证模式)
docker run -d --name osl-ferretdb \
  --network osl-5-0-0-net \
  -e FERRETDB_AUTH=false \
  -e FERRETDB_POSTGRESQL_URL="postgres://osl:q1w2e3r4@osl-postgres:5432/postgres" \
  --restart unless-stopped \
  ghcr.io/ferretdb/ferretdb:2.7.0

# 4. 应用层:OSL 5.0.0
docker run -d --name osl-app \
  --network osl-5-0-0-net \
  -p 8132:8080 \
  -e MONGODB_HOST=osl-ferretdb \
  --restart unless-stopped \
  ourshoppinglist/our-shopping-list:5.0.0

💡 说明:手工方式全部命令行参数都显式(没有 compose 的隐式默认),排障时更直观;但日常维护(升级、备份)建议仍用 compose 编排管理。上面 2~4 步的镜像、环境变量、依赖关系与 4.1 节编排完全等价,健康检查同样用 curl http://127.0.0.1:8132/healthcheck 验证。

十、常用运维命令

# 状态与健康
docker compose ps
curl http://127.0.0.1:8132/healthcheck

# 日志(应用 / 数据库层分别看)
docker compose logs app --tail 100
docker compose logs ferretdb --tail 50

# 改配置后重建应用服务
docker compose up -d app

# 资源占用(实测参考:app 39 MiB / ferretdb 11 MiB / postgres 49 MiB)
docker stats --no-stream $(docker compose ps -q)

10.1 升级(如 4.2.0 → 5.0.0)

cd /opt/osl
docker compose exec postgres pg_dump -U osl -d osl -F c -f /tmp/pre-upgrade.dump   # 先备份
docker compose pull
# 编辑 compose.yaml:把三个镜像 tag 换到目标版本(应用与 ferretdb/postgres 配套 tag 一起换)
docker compose up -d
curl http://127.0.0.1:8132/healthcheck

升级后确认版本:

docker compose logs app | grep APP_VERSION

实测回显(本次 5.0.0 部署):

APP_VERSION: '5.0.0'
  • v1 → v2 类数据结构迁移(老部署升级到多面板模型):除换镜像外需用内置 CLI 做一次清单归位,单面板用户流程是 VITE_APP_SINGLEBOARD_MODE=1 启动后执行 docker compose exec app node cli.js list:move-to-board --all --board <面板slug>;list:move-to-board 命令实测可用(docker compose exec app node cli.js list:move-to-board 4ff671d0-... --board mart 回显 {acknowledged: true, matchedCount: 1, modifiedCount: 1},清单的 boardId 随之变更)。
  • MongoDB 栈 → FerretDB 栈迁移:官方提供 mongodump/mongorestore 路线文档(见参考链接),操作对象是数据库层,应用配置只需把 MONGODB_HOST 指到新 ferretdb 服务。

10.2 卸载

cd /opt/osl
docker compose down            # 保留数据卷
docker compose down -v         # 连数据卷一起删(确认备份后)

实测回显(docker compose down -v,2026-10-24 实抓;不带 -v 的 down 为同一序列、仅少下方两条 Volume 记录):

 Container ourshoppinglist-5-0-0-stack-app-1      Stopping
 Container ourshoppinglist-5-0-0-stack-app-1      Stopped
 Container ourshoppinglist-5-0-0-stack-app-1      Removing
 Container ourshoppinglist-5-0-0-stack-app-1      Removed
 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Stopping
 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Stopped
 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Removing
 Container ourshoppinglist-5-0-0-stack-ferretdb-1  Removed
 Container ourshoppinglist-5-0-0-stack-postgres-1  Stopping
 Container ourshoppinglist-5-0-0-stack-postgres-1  Stopped
 Container ourshoppinglist-5-0-0-stack-postgres-1  Removing
 Container ourshoppinglist-5-0-0-stack-postgres-1  Removed
 Volume ourshoppinglist-5-0-0-stack_pgdbdata   Removing
 Network osl-5-0-0-net                          Removing
 Volume ourshoppinglist-5-0-0-stack_pgdbdata   Removed
 Network osl-5-0-0-net                          Removed

十一、常见问题

Q1:up 报 Bind for 0.0.0.0:8132 failed: port is already allocated,但 ss -ltn 看不到 8132 监听? 宿主机端口被其他 Docker 服务的映射占了(宿主 ss 看不到 docker-proxy 细节时容易误判)。用 docker ps --format '{{.Names}} {{.Ports}}' 排查占用者后换端口。本文实验过程中就遇到同机其他服务抢占 8128/8151 的情况,最终落位 8132。

Q2:日志里大量 manifest.json ENOENT 报错栈,服务是不是坏了? 不是。这是 5.0.0 镜像内 PWA 中间件的已知噪音,实测页面、PWA、API 一切正常,可安全忽略。

Q3:页面能开,但清单改动不同步? 九成是反向代理没转发 WebSocket。OSL 实时同步走 /socket.io/,Nginx 必须带升级头(官方片段,实测可用):

location / {
    proxy_set_header    Host $host;
    proxy_set_header    X-Real-IP $remote_addr;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto $scheme;
    proxy_http_version  1.1;
    proxy_set_header    Upgrade $http_upgrade;
    proxy_set_header    Connection "upgrade";
    proxy_pass          http://127.0.0.1:8132;
}

Q4:删除面板的入口是灰的/DELETE /boards 返回 403? 默认禁止删面板(防误删)。确需开放时设 VITE_APP_BOARD_DELETION_ENABLED=1 后重启应用服务,实测同一接口即从 403 变 200;删除会级联清掉面板下清单与条目,开放前先评估误删风险。

Q5:首页一片空白或报 /boards 403? 403 说明请求了「列出全部面板」接口但该能力默认关闭(VITE_APP_LIST_ALL_BOARDS_ENABLED=0)。这是正常安全默认:入口应走面板 slug URL(/family),或按需打开该变量。CLI 建面板入口:docker compose exec app node cli.js board:create family。

Q6:想强制所有人用中文界面? 设 VITE_APP_I18N_LOCALE=zh(跟随浏览器自动选择时保持默认,浏览器无匹配语言回退 VITE_APP_I18N_FALLBACK_LOCALE);要无视浏览器语言设置则再加 VITE_APP_I18N_FORCE_LOCALE=1。实测 VITE_APP_I18N_LOCALE=zh 生效后界面即中文。

十二、参考链接

本专栏其余篇目:选型与架构背景见《Our Shopping List 5.0.0 介绍与选型指南》;环境变量逐项实测见《Our Shopping List 5.0.0 配置参数详解》;接口调用见《Our Shopping List 5.0.0 API 使用详解》。