Our Shopping List Docker Compose 部署指南
用官方镜像与 Docker Compose 三服务栈(app + FerretDB + PostgreSQL)部署 Our Shopping List 5.0.0,覆盖部署、持久化、备份、升级与卸载全流程,全部步骤实测通过。
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.0 | MongoDB 协议 → PostgreSQL 翻译层 | 53.6 MB | 5.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 官方有迁移文档 |
选型结论:
- 应用镜像固定锁版本号(如
:5.0.0),不要用latest管理生产,升级时显式改 tag。 - 新部署一律选 FerretDB + PostgreSQL 栈:免 MongoDB SSPL 许可顾虑、备份回归 PostgreSQL 生态;官方仓库
doc/docker-compose.ferretdb.yml即此栈。 - FerretDB 与 postgres-documentdb 的版本是配套关系(postgres 镜像 tag 尾缀
ferretdb-2.7.0标明),升级 FerretDB 必须同步换存储镜像 tag。 - 已有 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_PORT | 8080 | 应用监听端口(改它时同步改 ports 映射右侧) |
MONGODB_HOST | mongodb | 数据库主机名,FerretDB 栈下填 ferretdb |
MONGODB_PORT | 27017 | 数据库端口 |
MONGODB_DB | osl | 数据库名 |
BASE_URL | 空 | 子路径部署时设(如 /my-osl/),见 8.2 节 |
应用键(VITE_APP_*,摘要——运行时经 /config.js 注入前端):
| 变量 | 默认值 | 说明 |
|---|---|---|
VITE_APP_I18N_LOCALE | en | 界面语言,zh 为简体中文 |
VITE_APP_I18N_FALLBACK_LOCALE | en | 回退语言 |
VITE_APP_I18N_FORCE_LOCALE | 0 | 1 = 强制忽略浏览器语言 |
VITE_APP_SINGLEBOARD_MODE | 0 | 1 = 单面板模式(v1 行为) |
VITE_APP_BOARD_DELETION_ENABLED | 0 | 1 = 允许删除面板 |
VITE_APP_LIST_ALL_BOARDS_ENABLED | 0 | 1 = 允许列出全部面板 |
VITE_APP_TITLE / VITE_APP_SHORT_TITLE | Our Shopping List / OSL | 站点标题/PWA 短标题 |
VITE_APP_HOME_MESSAGE | 空 | 首页提示语 |
VITE_APP_THEME | system | system/light/dark |
VITE_APP_LOCALSTORAGE_KEY_PREFIX | OurShoppingList_ | 浏览器存储键前缀 |
VITE_APP_SOCKETIO_PING_INTERVAL / _TIMEOUT / _CSR_MAXDISCONNECTIONDURATION | 25000 / 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 生效后界面即中文。
十二、参考链接
- 官方仓库与部署文档(README「With docker compose」节):https://codeberg.org/nanawel/our-shopping-list
- 官方 FerretDB 栈编排文件:https://codeberg.org/nanawel/our-shopping-list/raw/branch/master/doc/docker-compose.ferretdb.yml
- 官方遗留 MongoDB 栈编排文件:https://codeberg.org/nanawel/our-shopping-list/raw/branch/master/doc/docker-compose.mongodb.yml
- Mongo→FerretDB 官方迁移文档:https://codeberg.org/nanawel/our-shopping-list#migrate-from-mongodb-to-ferretdb
- 官方镜像:https://hub.docker.com/r/ourshoppinglist/our-shopping-list
- FerretDB 官方文档:https://docs.ferretdb.io/
本专栏其余篇目:选型与架构背景见《Our Shopping List 5.0.0 介绍与选型指南》;环境变量逐项实测见《Our Shopping List 5.0.0 配置参数详解》;接口调用见《Our Shopping List 5.0.0 API 使用详解》。