此方案在一台主机上运行完整产品:LMS、后台任务、实时通信和协作编辑服务,以及由配置档案控制的所有可选服务。架构设计见 docs/architecture/23-ops.md 第 2 节。
其他部署目标:Vercel和 Cloudflare Workers只运行 Web 层。升级步骤见 upgrade.md;备份和恢复演练见 backup-restore.md。
所需条件
- Docker Engine 27 或更高版本,以及 Compose 插件 2.30 或更高版本。
- 默认服务栈需要 4 个 CPU 核心和 8 GB 内存;使用
--profile full时需要 8 个核心和 16 GB 内存(仅 ClamAV 就会占用约 1.5 GB 签名数据)。 - Web 层需要一个 DNS 名称,不可信内容需要另一个名称。两者必须是不同的主机:SCORM 软件包和上传的 HTML 会在内容源站运行,因此永远无法读取 LMS Cookie。
- 本地测试可使用
lvh.me和*.localhost,两者都会解析到 127.0.0.1;docker/.env.example使用了这些名称。服务栈自带的proxy服务会使用本地证书颁发机构通过 https 提供两者,因此无需安装其他软件(参见“TLS”)。 - 主机上的 80 和 443 端口必须空闲(可用
QUIRE_PROXY_HTTP_PORT和QUIRE_PROXY_HTTPS_PORT更改端口)。
首次运行
QUIRE_APP_ORIGIN=https://learn.example.org \
QUIRE_CONTENT_ORIGIN=https://content.example-content.org \
QUIRE_SETUP_ADMIN_EMAIL=you@example.org \
docker/scripts/init-env.sh
docker compose -f docker/compose.yaml up -d --build
docker compose -f docker/compose.yaml logs initdocker/scripts/init-env.sh 会写入 docker/.env,初始内容取自 docker/.env.example,生成所有密钥(数据库密码、签名密钥和主密钥、内容启动密钥对),并在 docker/secrets/audit-signing-key.pem 中生成审计检查点签名密钥;Compose 会将该文件作为密钥挂载到 worker。此脚本只需要 sh、awk 和 openssl,如果已有 docker/.env 则会拒绝覆盖。请将这两个文件都复制到主机之外:没有 QUIRE_MASTER_KEY,恢复的数据库就无法解密其中保存的凭据。若要手动填写,可以运行 cp docker/.env.example docker/.env;文件中说明了每个密钥的生成方法。
两个源站都必须使用 https:内容服务在生产环境中拒绝普通 http,两者也不能共享同一个可注册域名。proxy 服务会为两者终止 TLS(参见“TLS”);init-env.sh 会拒绝 http:// 源站。
服务栈按固定顺序启动;每个步骤都会等待前一步完成:
postgres状态变为健康。首次启动时,其初始化脚本(docker/postgres/init/90-passwords.sh)会设置四个角色密码。migrate会应用所有迁移,并在控制数据库及每个专用租户数据库中初始化任务队列,然后检查它们是否一致,最后退出(参见 docs/ops/upgrade.md)。每次启动都会运行迁移,且迁移具有幂等性,因此升级只需换用新镜像并重启。init(apps/web/src/first-run.ts)会将应用数据库登记为QUIRE_DATABASE_ID;如果设置了QUIRE_SETUP_ADMIN_EMAIL,还会创建首个组织及其管理员。登录地址和生成的密码只会输出一次,可在docker compose logs init中查看。web、content、worker、scheduler、collab和centrifugo启动。proxy会在web和content健康后启动。
打开 https://demo. 加上应用域名(init 日志会打印准确的登录地址)并登录。本地安装时,请先信任 proxy 的证书颁发机构(参见“TLS”)。在 /account/security 更改生成的密码。
如果进程缺少必需密钥,就会拒绝启动并在日志中指出缺少的设置。系统不会以未完成配置的状态启动。
服务和配置档案
| 服务 | 配置档案 | 用途 |
|---|---|---|
| postgres | 始终 | 数据库(PostgreSQL 18,含 pgvector,由 docker/postgres.Dockerfile 构建),从首次启动开始归档 WAL |
| migrate、init | 始终 | 一次性执行:数据库迁移,然后首次初始化 |
| web | 始终 | LMS,监听 QUIRE_HTTP_PORT(8080) |
| content | 始终 | 不可信内容源站,监听 QUIRE_CONTENT_PORT(8081) |
| worker | 始终 | 后台任务:电子邮件、报告、文件处理、webhook |
| scheduler | 始终 | 定期任务:注册 64 个运行时计划并交给 worker;同时只有一个领导者 |
| collab | 始终 | 协作编辑 websocket,监听 QUIRE_COLLAB_HTTP_PORT(1234) |
| centrifugo | 始终 | 实时扇出服务,监听 QUIRE_REALTIME_PORT(8000) |
| proxy | 始终 | Caddy,监听 80 和 443 的 TLS 入口(参见“TLS”) |
| valkey | cache |
缓存和速率限制 |
| clamav | scan |
扫描上传文件中的恶意软件 |
| gotenberg | preview |
将 Office 文件预览为 PDF,并渲染证书 |
| imgproxy | images |
调整图片尺寸和格式 |
| transcoder | video |
使用仅含 LGPL 的 ffmpeg 的 worker 镜像,为视频生成不同版本 |
| seaweedfs | storage |
此主机上的 S3 兼容对象存储 |
| otelcol | observability |
OpenTelemetry 收集器 |
| mailpit | devmail |
捕获所有外发邮件,供测试 Quire 使用 |
| backup | backup |
一次性基础备份;参见 backup-restore.md |
| backup-scheduler、backup-offsite | backup |
每隔 QUIRE_BACKUP_INTERVAL_HOURS 创建基础备份,并创建加密的异地副本及每周验证演练 |
| h5p | h5p |
您通过 QUIRE_H5P_IMAGE 提供的 H5P LTI 1.3 工具镜像,通过 QUIRE_H5P_PORT(8090)运行;参见“连接 H5P 服务提供商” |
--profile full 会启动除 backup 和 h5p 外的所有可选服务。运行 docker compose -f docker/compose.yaml --profile scan up -d 可启动单个服务。即使没有可选服务,Quire 仍可运行,并会说明缺少的功能:没有扫描器时,上传文件会未经扫描就保存,并会通知管理员;没有 Gotenberg 时,文件只能下载而不会生成预览;没有转码器时,视频会以原始文件播放。
所有第三方镜像及其许可证义务均列在 docker/third-party-containers.yaml 中。
连接 H5P 服务提供商
Quire 不嵌入或附带 H5P 运行时或 sidecar(ADR 0019)。如果使用 H5P,请自行订阅托管服务,或单独运行自托管 H5P 实例。将该服务提供商注册为 LTI 1.3 外部工具,并以工具活动形式将内容添加到课程中。Quire 通过 LTI Assignment and Grade Services(AGS)交换成绩和活动/评分进度。如果服务提供商也发送 xAPI statements,请单独配置 Quire 的 xAPI 语句存储;AGS 成绩/进度交换不会发送 xAPI statements。Moodle 导入会将 H5P 活动标记为需要 LTI 工具连接。服务提供商仍负责其 H5P 运行时、内容编写、内容库和作答历史。
若要在此主机运行自己的自托管实例,请将 QUIRE_H5P_IMAGE 设为相应镜像并启动 h5p 配置档案。Compose 会通过 QUIRE_H5P_PORT(8090)发布服务,并将数据保存在 h5p-data 卷中;镜像及其相关义务都由您负责。
设置
所有进程都读取 docker/.env。模板 docker/.env.example 会列出每项设置及其默认值。设置分组如下:
地址
| 设置 | 含义 |
|---|---|
QUIRE_APP_ORIGIN |
LMS 的公共地址,例如 https://learn.example.com |
QUIRE_CONTENT_ORIGIN |
内容源站,必须是不同主机 |
QUIRE_PLATFORM_DOMAINS |
组织所在的域名,多个域名用逗号分隔 |
QUIRE_MARKETING_ORIGIN |
Optional. The marketing site, default https://quirelms.com. The only origin the waitlist form (POST /api/waitlist, POST /waitlist) accepts and redirects to. Comma separated; a www. variant is allowed only if listed |
QUIRE_DEPLOY_TARGET |
此处设为 compose。其他部署目标见 vercel 和 cloudflare 指南 |
QUIRE_TRUSTED_PROXY_CIDRS |
可信任其 X-Forwarded-For 标头的代理 |
密钥
| 设置 | 含义 |
|---|---|
QUIRE_SECRET_KEY |
为会话和令牌签名,64 个十六进制字符 |
QUIRE_MASTER_KEY |
包装 SSO 和 webhook 密钥等已存储凭据,32 字节、base64 编码。Web 层和 worker 必须使用同一个值。轮换方法见 key-rotation.md |
QUIRE_MASTER_KEY_VERSION |
主密钥的版本标签;未设置时为 v1。轮换时递增版本 |
QUIRE_MASTER_KEY_RETIRED |
仍需用于读取旧密钥所包装内容的主密钥,例如 v1=<base64>。所有未解决内容都完成轮换后再移除 |
QUIRE_COLLAB_SIGNING_KEY |
web 和 collab 共用,用于为编辑令牌签名 |
QUIRE_BACKUP_SIGNING_KEY |
为课程备份签名(可选) |
请在本主机之外保存一份 QUIRE_MASTER_KEY 副本。没有此密钥,恢复的数据库无法解密其中保存的凭据。
数据库
| 设置 | 含义 |
|---|---|
POSTGRES_PASSWORD |
超级用户密码,供容器和备份使用 |
QUIRE_DB_APP_PASSWORD、QUIRE_DB_MIGRATOR_PASSWORD、QUIRE_DB_REPORT_PASSWORD、QUIRE_DB_AUDIT_PASSWORD |
首次启动时设置的角色密码 |
DATABASE_URL |
应用角色。其执行的每条查询都受行级安全限制 |
DATABASE_MIGRATOR_URL、QUIRE_MIGRATION_URL |
供 migrate 和 init 使用的迁移角色 |
QUIRE_SUPERUSER_URL |
仅供首次初始化使用 |
QUIRE_REPORT_DATABASE_URL |
供报告和报告构建器使用的只读报告角色 |
QUIRE_AUDIT_DATABASE_URL |
供审计控制台和 SIEM 导出使用的审计角色 |
QUIRE_DATABASE_ID |
任意 UUID,在整个安装生命周期内固定不变 |
只有首次创建数据库卷时才会应用角色密码。之后要更改密码,请使用 ALTER ROLE,然后更新对应的 URL。
QUIRE_REPORT_DATABASE_URL 用于 DATABASE_URL 指定的物理数据库。对于任何其他已注册物理数据库,请在 web 和 worker 环境中设置其专属的 quire_report 连接 URL,然后在该数据库的报告环境变量字段中以 env:NAME 形式填写变量名。该变量必须指向与应用连接相同的数据库,最好使用只读副本。所有报告界面都会根据租户使用其数据库自己的报告连接:报告构建器和已保存报告、计划投递、报告导出、分析、审计日志、REST 审计资源以及助手的审计搜索都包括在内。任何功能都不会借用其他数据库的报告 URL。数据库没有报告连接时,普通报告会使用该数据库自身的应用连接运行;分析和所有审计读取则会拒绝操作并说明原因,因为应用角色无法读取审计记录。
驱动
| 设置 | 本版本支持 | 说明 |
|---|---|---|
QUIRE_STORAGE_DRIVER |
local(默认)、s3 或 azure |
local 会将文件保存到 files 卷中。s3 支持 AWS S3、R2、GCS 互操作性和其他 S3 兼容存储,并支持可续传的分段上传 |
QUIRE_REALTIME_DRIVER |
inprocess(默认)、sse、centrifugo 或 durable_objects |
单个 web 容器适合使用 inprocess;存在多个容器时请用 centrifugo 或 sse |
QUIRE_CACHE_DRIVER |
memory(默认)、postgres 或 valkey |
memory 是每个进程独立的缓存;使用 valkey 或 postgres 可让速率限制在多个容器间生效 |
QUIRE_VIDEO_DRIVER |
ffmpeg(默认)或 progressive_mp4 |
也可通过密钥使用托管服务提供商:Cloudflare Stream、Mux 或 Bunny |
QUIRE_IMAGE_DRIVER |
noop(默认)、imgproxy 或 cloudflare |
noop 会按原始大小提供所有图片。imgproxy 需要启用 images 配置档案并填写下方设置;cloudflare 使用 Cloudflare Images |
QUIRE_MEETING_PROVIDER |
bbb、zoom、teams、meet、jitsi 或 in_process |
平台默认的实时会议服务提供商。未设置时,实时会议会显示未配置,直到组织在“集成”下的“实时会议服务提供商”页面连接自己的账户。组织账户始终优先于此值。只有此处指定服务提供商对应的配置才会读取(BBB_URL 和 BBB_SECRET,或 ZOOM_*、TEAMS_*、GOOGLE_MEET_* 和 JITSI_* 变量) |
QUIRE_MEETING_REGIONS |
以逗号分隔的 eu、uk、us 列表 |
平台默认服务提供商处理会议数据的区域。未设置时,不会像之前一样针对固定到某区域的组织进行检查。组织账户应在其页面中声明数据区域 |
如果设置了本版本不包含的驱动值,Web 层启动时会拒绝运行并指出该设置,而不会悄悄换用默认驱动。
图片
页面会通过 /api/files/{id}/image/{size} 请求四种固定尺寸的图片;此端点会按文件本身相同的访问规则进行检查,然后重定向到图片服务。每个组织每小时最多可请求 QUIRE_IMAGE_SPECS_PER_HOUR(默认 2000)个新的图片和尺寸组合;该小时内已经生成过的尺寸不计入。多个 web 容器共用时,请使用 valkey 或 postgres 作为 QUIRE_CACHE_DRIVER,确保各容器共享此限制。
| 设置 | 驱动 | 说明 |
|---|---|---|
IMGPROXY_URL |
imgproxy |
浏览器访问 imgproxy 的地址,例如 https://images.example.org。images 配置档案会在 QUIRE_IMAGES_PORT(8082)发布服务 |
IMGPROXY_KEY、IMGPROXY_SALT |
imgproxy |
十六进制字符串,必须与启动 imgproxy 时使用的值相同。分别使用 openssl rand -hex 32 生成。Quire 会使用它们为每个图片地址签名,因此 imgproxy 不会渲染 Quire 未请求的内容 |
QUIRE_IMAGE_SOURCE_ORIGIN |
使用本地存储的 imgproxy |
imgproxy 获取原始图片的地址。Compose 会设置为 http://web:3000。如果使用 s3 或 azure 存储,imgproxy 会从存储桶获取图片,因此不使用此设置 |
CLOUDFLARE_ACCOUNT_ID、CLOUDFLARE_IMAGES_TOKEN、CLOUDFLARE_IMAGES_ACCOUNT_HASH |
cloudflare |
具有 Images 编辑权限的 API token,以及 Images > Developer resources 中的账户哈希。请为账户启用灵活变体 |
CLOUDFLARE_IMAGES_SIGNING_KEY |
cloudflare |
可选。设置后图片为私有,每个地址都会签名并设有过期时间。未设置时,图片会以从 QUIRE_SECRET_KEY 派生的、无法猜测的地址公开提供 |
Cloudflare Images 会为其提供的每张原始图片保留自己的副本。删除文件时,worker 会先删除该副本,然后再删除原始文件。
队列
后台任务使用同一 Postgres 数据库中的 pg-boss,因此无需运行队列服务,也无需进行配置。任务会与触发任务的更改在同一个事务中加入队列,因此崩溃不会导致任务丢失或重复发送。此处 QUIRE_QUEUE_DRIVER 默认为 pgboss;vercel 和 cloudflare 只会将轻量通知和 webhook 投递转移到平台自己的队列。Vercel 和 Cloudflare 指南介绍了相关功能以及 Web 层如何加入队列。
电子邮件
设置以下其中一项:
QUIRE_EMAIL_PROVIDER_CONFIG:用于指定 HTTP 服务提供商及凭据的 JSON 对象,例如{"provider":"postmark","token":"..."}。支持 Postmark、Amazon SES、Mailgun、SendGrid 和 Resend。QUIRE_SMTP_URL:smtp://user:password@host:587。仅此部署目标支持;无服务器部署目标会阻止 SMTP。
QUIRE_MAIL_FROM 用于设置发件人。要试用 Quire,请启动 devmail 配置档案,将 QUIRE_SMTP_URL=smtp://mailpit:1025 设为 SMTP 地址,并在 http://localhost:8025 查看邮件。
可选服务
| 设置 | 配置档案 |
|---|---|
CLAMAV_URL=tcp://clamav:3310 |
scan |
GOTENBERG_URL=http://gotenberg:3000 |
preview |
IMGPROXY_KEY、IMGPROXY_SALT |
images |
VALKEY_URL=redis://valkey:6379 |
cache |
QUIRE_OPENSEARCH_URL 或 QUIRE_MEILISEARCH_URL |
外部搜索;否则使用 Postgres 全文搜索 |
QUIRE_BREACH_CHECK_PROVIDER=off、QUIRE_BREACH_CHECK_URL |
密码泄露检查。默认启用并使用 api.pwnedpasswords.com(只发送五个字符的哈希前缀);设为 off 可关闭,填写 URL 则使用您托管的范围 API |
可观测性
OTEL_EXPORTER_OTLP_ENDPOINT 指定所有进程发送跟踪和指标的收集器;启用 observability 配置档案时为 http://otelcol:4318。要为您的后端添加导出器,请编辑 docker/otel-collector.yaml。设置此值后,Web 层、worker、scheduler、content 和 collab 进程会通过 OTLP/HTTP 导出跨度(Web 请求、租户数据库事务、worker 任务和外发调用),并每分钟向相同端点发送指标(OTEL_METRICS_EXPORTER=none 可关闭指标)。OTEL_TRACES_SAMPLER_ARG 设置保留跟踪的比例。日志会按 LOG_LEVEL 输出到标准输出,Compose 会轮换日志。跟踪中不会包含个人数据。
区域外发流量(欧盟数据驻留)
QUIRE_REGION=eu 表示此服务栈为欧盟组织提供服务。之后,worker 会将代表固定在欧盟区域的组织发起的所有外部请求限制在允许名单内(21-compliance.md 第 8.1 节)。允许名单包括已配置服务声明的区域主机(存储端点、电子邮件服务提供商、托管视频服务提供商、组织自己的存储目标、AI 服务提供商和电子邮件账户)、有效例外批准中列出的服务主机,以及您在 QUIRE_EGRESS_ALLOW_HOSTS 中填写的主机。对任何其他公共主机的请求都会在发送前被拒绝,拒绝记录会以 privacy/egress_refused 写入组织审计记录,并显示在“合规”>“数据驻留”页面。
| 设置 | 值 | 效果 |
|---|---|---|
QUIRE_EGRESS_ALLOW_HOSTS |
以逗号分隔的主机名列表,或 *.example.org 形式的所有子域名 |
欧盟组织可以访问的额外主机。Webhook、xAPI 和 SIEM 端点、博客源以及 Amazon SES 主机属于此处,因为这些由组织自行选择,且没有服务为其声明区域。本地回环、私有地址和 web 或 clamav 这样的单标签名称属于您自己的网络,始终不会检查 |
英国和美国组织不受主机名单限制,但仍执行服务区域检查。请在 worker 上设置该名单;管理页面会在 Web 层读取此值以显示允许名单,因此请将其写入所有服务都会读取的 docker/.env。
应用层检查会给出清晰错误并写入审计记录,但它不是保证,因为代码可能出错;真正的保证来自网络。Compose 不会替您强制执行网络规则。要构建区域化服务栈,请将 worker 和 web 服务放在 internal: true 网络上,让唯一的外部路由经过一个允许访问与 QUIRE_EGRESS_ALLOW_HOSTS 相同主机及已配置服务主机的出口代理(例如 Squid 或 tinyproxy 容器),并为这些服务设置 HTTPS_PROXY。数据驻留页面会列出应用允许访问的准确主机,方便您比对两份名单。
健康检查
| 端点 | 含义 |
|---|---|
/healthz |
存活状态:进程能够响应。Compose 健康检查使用此端点 |
/readyz |
就绪状态:依赖项可访问,并会报告每项可选服务是否已配置。负载均衡器应使用此端点 |
运行 docker compose -f docker/compose.yaml ps 可查看每项服务的健康状态。
TLS
proxy 服务(Caddy,Apache-2.0,配置文件为 docker/caddy/Caddyfile)属于默认服务栈。它监听 80 和 443 端口,并按如下规则路由:
| 主机或路径 | 转发到 |
|---|---|
QUIRE_PROXY_CONTENT_HOST |
content |
QUIRE_PROXY_APP_HOST、所有租户子域名和自定义域名 |
web |
这些主机上的 /_collab/ |
collab(websocket,QUIRE_COLLAB_URL) |
这些主机上的 /_realtime/connection/ |
centrifugo 的客户端 websocket;其服务器 API 永不对外公开 |
这些主机上的 /_images/ |
imgproxy;需要启用 images 配置档案(IMGPROXY_URL) |
init-env.sh 会根据两个源站推导 QUIRE_PROXY_APP_HOST、QUIRE_PROXY_CONTENT_HOST、QUIRE_PROXY_HTTPS_PORT、QUIRE_COLLAB_URL 和 IMGPROXY_URL,避免配置彼此不一致。手动更改源站时,请一并修改这些值。
证书由 QUIRE_PROXY_TLS 控制:
-
internal(默认值):使用 Caddy 自带的证书颁发机构,适用于localhost、*.localhost和lvh.me。信任其根证书后即可浏览:docker compose -f docker/compose.yaml cp \ proxy:/data/caddy/pki/authorities/local/root.crt ./quire-local-ca.crt将
quire-local-ca.crt添加到系统或浏览器的信任存储中。curl可使用--cacert指定该证书。 -
设置电子邮件地址:为真实主机名自动申请 ACME 证书(先用 Let’s Encrypt,再用 ZeroSSL)。两个源站及所有租户主机的 DNS 都必须指向此处,并且互联网必须可以访问 80 和 443 端口。
首次访问租户主机时会按需颁发证书,并且只有在 Web 层确认该名称属于此安装(通过 Compose 网络调用 /tls-allowed)时才会颁发。无需通配符证书或 DNS 服务商插件;陌生人即使将名称指向此主机,也无法诱使它申请证书。证书和本地证书颁发机构保存在 caddy-data 卷中;如果使用 internal,请与其他数据一起备份此卷。
Web 层只会信任来自代理的 X-Forwarded-For:代理使用固定地址(QUIRE_PROXY_ADDRESS,默认 172.29.64.10),位于固定子网(QUIRE_COMPOSE_SUBNET)上;QUIRE_TRUSTED_PROXY_CIDRS 会列出该地址。如果此子网与主机网络冲突,请同时更改两项设置,并先运行 docker compose down,再运行 up。
位于自有反向代理之后
要使用已有的负载均衡器或代理,请不启动 proxy(运行 docker compose up -d --scale proxy=0),并在 web(8080)、content(8081)、collab(1234,websocket)和 centrifugo(8000,websocket)前终止 TLS。设置公共地址 QUIRE_APP_ORIGIN、QUIRE_CONTENT_ORIGIN 和 QUIRE_COLLAB_URL(wss://),并在 QUIRE_TRUSTED_PROXY_CIDRS 中填写代理的地址范围。
故障排查
init因“QUIRE_DATABASE_ID is not a UUID”退出:使用uuidgen设置该值。web因“did not start on compose”不断重启:日志会列出每项无法接受的设置及建议替代值。- 首次启动后更改
.env中的角色密码没有效果:初始化脚本只运行一次。请使用ALTER ROLE。 - 设置了
CLAMAV_URL时上传因扫描错误失败:ClamAV 首次启动时会下载签名数据,这需要几分钟。