Papermerge 3.5.3 Docker Compose 部署指南
Papermerge 3.5.3 六服务 Docker Compose 全栈部署实录:登录/上传/OCR/全文检索闭环、持久化布局、环境变量表与七个排障实录
Papermerge 3.5.3 Docker Compose 部署指南
目标:在一台 Linux 服务器上,用一份 compose 文件把 Papermerge 3.5.3 的完整生产形态(webapp + PostgreSQL + Redis + Solr + OCR worker + i3 worker 六服务)部署起来,并以实测闭环(登录 → 建目录 → 上传扫描件 → OCR → 全文检索命中)验证部署成功。
组件定位与选型见站内《Papermerge 介绍与选型指南》;参数逐项详解见本专栏《Papermerge 3.5.3 配置参数详解》;REST API 用法见本专栏《Papermerge 3.5.3 API 使用详解》。本文全部命令与回显均为 2026-11-01/02 实测照抄。
一、环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Linux(x86_64,内核 7.0.0-38-generic) |
| 部署工具 | Docker Compose(compose spec 支持 depends_on.condition) |
| 应用版本 | Papermerge 3.5.3(官方当前最新稳定版) |
| 对外端口 | 8138(映射到 webapp 的 80) |
| 内部网段 | 10.89.57.0/24(compose 项目专用子网) |
| 登录账号 | admin / q1w2e3r4(示例值,部署时请替换) |
| 组件全栈 | papermerge/papermerge:3.5.3、papermerge/ocrworker:0.3.1、papermerge/i3worker:0.3、postgres:16.1、redis:7.2、solr:9.7 |
组件版本与官方 3.5.3 版本矩阵一致(papermerge-core 官方文档 admin-manual/version-matrix,查询日期 2026-11-01)。auth-server 无需单独部署:3.5.3 的 webapp 镜像内部已自带基础认证服务并随 supervisord 一起拉起,实测 webapp 启动日志可见 nginx + uvicorn(:8000) + auth-server(:4010) 同进程组共存(见 4.5 节)。
二、部署架构与组件选型
官方设计哲学是"渐进式部署":从最小 webapp 起步,按需逐个加 PostgreSQL、media 持久化、Redis、OCR worker、Solr + i3 worker、S3/path-tmpl worker(来源:官方文档 setup/docker-compose,查询日期 2026-11-01)。本文一步到位部署官方文档 I3 + OCR Workers 终态组合,即生产推荐的完整形态:
浏览器 ──▶ :8138 ──▶ webapp(nginx:80 + Django:8000 + 内置 auth-server:4010)
│ │ │
元数据 │ 任务队列(Redis) │ 索引读写
▼ ▼ ▼
db redis solr(core: pmg)
(postgres:5432) (:6379) (:8983)
▲ ▲ ▲
│ ┌─────┴─────┐ │
└──┤ ocr_worker │ │(celery 队列 ocr)
├────────────┤ │
│ i3worker │──────┘(celery 队列 i3,DB→Solr 同步)
└────────────┘
webapp 与 ocr_worker 共享 bind mount:./data/media → /var/media/pmg
选型要点(均为官方版本矩阵与本文实测结论):
- 数据库:webapp 默认回落 SQLite(
sqlite:////db/db.sqlite3),生产必须显式设置PAPERMERGE__DATABASE__URL指向 PostgreSQL;三个应用组件(webapp/ocr_worker/i3worker)必须使用同一个数据库 URL。 - 检索:默认本地 Xapian;本例用 Solr(
solr://solr:8983/pmg),由 i3worker 负责把数据库内容同步进索引。Solr core 的 schema 必须预置补丁,否则全文检索恒 0 命中(见第九章问题 4,这是本次部署最大的坑)。 - 队列:Redis 作为 Celery broker,队列名固定
ocr/i3(worker 启动横幅实测[queues] .> ocr exchange=ocr(direct) key=ocr)。 - media 共享:上传的原始文件与 OCR 产物都落在文件系统 media root,不进数据库;webapp 与 ocr_worker 必须挂载同一份 media 目录。
三、前置条件
- Docker 与 Docker Compose 已安装(
docker compose version可用)。 - 宿主机端口 8138 空闲;若与其他自建栈共用 Docker 网络资源,建议像本文一样为项目显式指定子网,避免网段漂移。
- 磁盘:本例部署闭环跑完(3 个文档、OCR 若干次)后的持久化目录占用实测为:
2.0M data/media
48M data/pgdata
1.9M data/solr
- 准备两个验证文件(可任意):一个纯图像无文本层的扫描 PDF(测 OCR),一个自带文本层的 PDF(测索引)。本例用自制的
invoice-scan.pdf(81232 字节,德文发票扫描件,含关键词KAFFEEMUEHLE)与notes-text.pdf(631 字节,文本层含标记778811)。
四、部署步骤
4.1 准备 Solr core 配置补丁(必做,先于首次启动)
官方 compose 示例用 solr-precreate pmg(默认 configset)建 core,但默认 schema 没有到 _text_ 的 copyField,而 Papermerge 的检索组件是按"裸词 + df=text"查询的——照官方示例部署,OCR 再成功全文检索也永远 0 命中(诊断过程见第九章问题 4)。正确做法是准备一份打过补丁的 configset 目录,首启即生效。
本项目目录结构:
stack/
├── docker-compose.yml
├── solr-config/ # 打过补丁的 configset(挂载进 solr 供 solr-precreate 使用)
│ ├── managed-schema.xml
│ ├── solrconfig.xml
│ ├── lang/ ...
└── data/ # bind mount 持久化:media / pgdata / solr
solr-config/managed-schema.xml 相对默认 configset 的两处关键改动(实测该文件生效内容):
<field name="page_number" type="pint" multiValued="false" indexed="true" stored="true"/>
...
<copyField source="*_txt_en" dest="_text_"/>
copyField *_txt_en → _text_:让动态规则命中的title_txt_en/text_txt_en汇入_text_,裸词查询才能命中(问题 4 的修复);page_number定义为单值pint:Papermerge 的索引组件 salinic 以单值 NumericField 写入该字段,若沿用默认多值定义,旧段与新段 docValues 类型冲突会导致文档写入被拒(问题 6 的修复)。
补丁做法二选一:像本文一样在首启前备好 configset 目录;或先用默认 configset 起,再走 Schema API 修改 + 清段 + 重建索引(步骤繁琐且要 force-recreate,见问题 6)。强烈建议前者。
4.2 编写 compose 文件
stack/docker-compose.yml 全文(即本次实测使用的文件,仅 SECRET_KEY 与密码为示例值):
name: papermerge-353-stack
services:
webapp:
image: papermerge/papermerge:3.5.3
environment:
PAPERMERGE__SECURITY__SECRET_KEY: "kX9vT2mQm4pR7wLz0cJdNfHbYs6uG1eA3iO5rT8yW0qZ4xC7vB9nM2kJ5pR8tY1u"
PAPERMERGE__AUTH__USERNAME: admin
PAPERMERGE__AUTH__PASSWORD: q1w2e3r4
PAPERMERGE__AUTH__EMAIL: admin@example.com
PAPERMERGE__DATABASE__URL: postgresql://pmuser:q1w2e3r4@db:5432/pmgdb
PAPERMERGE__MAIN__MEDIA_ROOT: /var/media/pmg
PAPERMERGE__REDIS__URL: redis://redis:6379/0
PAPERMERGE__SEARCH__URL: solr://solr:8983/pmg
PAPERMERGE__OCR__LANG_CODES: "deu,eng,ron"
PAPERMERGE__OCR__DEFAULT_LANG_CODE: "deu"
volumes:
- ./data/media:/var/media/pmg
ports:
- "8138:80"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
solr:
condition: service_started
ocr_worker:
image: papermerge/ocrworker:0.3.1
command: worker
environment:
PAPERMERGE__DATABASE__URL: postgresql://pmuser:q1w2e3r4@db:5432/pmgdb
PAPERMERGE__REDIS__URL: redis://redis:6379/0
PAPERMERGE__MAIN__MEDIA_ROOT: /var/media/pmg
OCR_WORKER_ARGS: "-Q ocr -c 2"
volumes:
- ./data/media:/var/media/pmg
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
i3worker:
image: papermerge/i3worker:0.3
command: worker
environment:
PAPERMERGE__DATABASE__URL: postgresql://pmuser:q1w2e3r4@db:5432/pmgdb
PAPERMERGE__SEARCH__URL: solr://solr:8983/pmg
PAPERMERGE__REDIS__URL: redis://redis:6379/0
I3_WORKER_ARGS: "-Q i3 -c 2"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
solr:
condition: service_started
db:
image: postgres:16.1
volumes:
- ./data/pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: q1w2e3r4
POSTGRES_DB: pmgdb
POSTGRES_USER: pmuser
healthcheck:
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
interval: 5s
timeout: 10s
retries: 5
start_period: 10s
redis:
image: redis:7.2
solr:
image: solr:9.7
volumes:
- ./data/solr:/var/solr
- ./solr-config:/opt/pmgschema:ro
command:
- solr-precreate
- pmg
- /opt/pmgschema
networks:
default:
ipam:
config:
- subnet: 10.89.57.0/24
与官方示例的差异说明:
solr服务多挂了一个只读目录并把 configset 作为solr-precreate第三参数传入——即 4.1 的补丁,官方示例没有这一步(坑源)。- 持久化一律用 bind mount(
./data/...)而非命名卷,便于直接盘点与备份;生产也可换命名卷。 redis/solr未对外发布端口,仅内部网络可达;Solr 直查需走内部地址(见 4.7 与第九章)。PAPERMERGE__OCR__LANG_CODES按官方文档配置了 UI 语言下拉;实测该键在 3.5.3 无任何消费代码,属"写了不报错但不起作用"的配置(详见《Papermerge 3.5.3 配置参数详解》OCR 节与第九章问题 3 的白名单实测)。
4.3 拉取镜像
cd stack
docker compose pull
6 个镜像全部拉取成功(拉取日志尾行示例):
Image solr:9.7 Pulled
4.4 启动
docker compose up -d
首启时序(实测各服务日志):
db先初始化 PostgreSQL 集群并进入 healthy:
db-1 | The files belonging to this database system will be owned by user "postgres".
db-1 | This user must also own the server process.
db-1 | The database cluster will be initialized with locale "en_US.utf8".
db-1 | fixing permissions on existing directory /var/lib/postgresql/data ... ok
db-1 | creating subdirectories ... ok
db-1 | running bootstrap script ... ok
db-1 | performing post-bootstrap initialization ... ok
db-1 | syncing data to disk ... ok
db-1 | Success. You can now start the database server using:
db-1 | pg_ctl -D /var/lib/postgresql/data -l logfile start
db-1 | waiting for server to start....2026-11-01 16:07:01.985 UTC [48] LOG: starting PostgreSQL 16.1 (Debian 16.1-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
solr用挂载的 configset 预建 core:
solr-1 | Executing /opt/solr/docker/scripts/solr-precreate pmg
solr-1 | Executing /opt/solr/docker/scripts/precreate-core pmg
solr-1 | Created pmg
solr-1 | Starting Solr
solr-1 | 2026-11-01 16:12:29.711 INFO (main) [c: s: r: x: t:] o.a.s.s.CoreContainerProvider / __| ___| |_ _ Starting in standalone mode on port 8983
solr-1 | 2026-11-01 16:12:29.712 INFO (main) [c: s: r: x: t:] o.a.s.s.CoreContainerProvider |___/\___/_|_| Start time: 2026-11-01T16:12:29.712236837Z
webapp自动跑数据库迁移、自动创建超级用户(无需手工初始化):
webapp-1 | INFO [alembic.runtime.migration] Running upgrade -> bc29f69daca4, Initial migration
webapp-1 | INFO [alembic.runtime.migration] Running upgrade 4756d6d67b0d -> ad7145f8e1d5, v3.5.3: 0002 - Set home_folder_id and inbox_folder_id to NOT NULL
webapp-1 | INFO [alembic.runtime.migration] Running upgrade ad7145f8e1d5 -> 999b99441bd6, v3.5.3: 0003 - Set node_user_id foreign key constraint as deferrable
webapp-1 | INFO [alembic.runtime.migration] Running upgrade 999b99441bd6 -> 50ef5a403d27, v3.5.3: 0004 -Add non-empty constraint to role name
webapp-1 | User admin created
ocr_worker以 Celery 形态监听ocr队列(root 运行的 SecurityWarning 为官方镜像固有行为,可忽略):
ocr_worker-1 | -------------- celery@6925c7f736f0 v5.4.0 (opalescent)
ocr_worker-1 | --- ***** -----
ocr_worker-1 | -- ******* ---- Linux-7.0.0-38-generic-x86_64-with-glibc2.36 2026-11-01 16:07:09
ocr_worker-1 | - ** ---------- .> app: ocrworker:0x71556e7a81a0
ocr_worker-1 | - ** ---------- .> transport: redis://redis:6379/0
ocr_worker-1 | - ** ---------- .> results: redis://redis:6379/0
ocr_worker-1 | - *** --- * --- .> concurrency: 2 (prefork)
ocr_worker-1 | -------------- [queues]
ocr_worker-1 | .> ocr exchange=ocr(direct) key=ocr
注意:首启日志里 webapp 的
exec_index_schema_apply步骤可能因 Solr 尚未就绪而打印一段连接异常 traceback 后继续启动(实测如此)。只要 4.1 的 configset 补丁到位、i3worker 正常,这个失败不影响最终检索;若首启后检索不通,优先按第九章问题 7 处理,不要去 webapp 里补跑paper-cli index-schema apply。
4.5 验证服务状态
docker compose ps --format 'table {{.Service}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
实测回显(六服务全部 Up):
SERVICE IMAGE STATUS PORTS
db postgres:16.1 Up 12 hours (healthy) 5432/tcp
i3worker papermerge/i3worker:0.3 Up 12 hours
ocr_worker papermerge/ocrworker:0.3.1 Up 12 hours
redis redis:7.2 Up 12 hours 6379/tcp
solr solr:9.7 Up 3 hours 8983/tcp
webapp papermerge/papermerge:3.5.3 Up 12 hours 0.0.0.0:8138->80/tcp, [::]:8138->80/tcp
Web 入口与接口探活:
curl -sI http://127.0.0.1:8138/
curl -s http://127.0.0.1:8138/ | grep -oE '<title>[^<]*</title>'
curl -s http://127.0.0.1:8138/api/version/ -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w 'HTTP=%{http_code} bytes=%{size_download}\n' \
http://127.0.0.1:8138/api/probe/ -H "Authorization: Bearer $TOKEN"
实测回显:
HTTP/1.1 200 OK
Server: nginx/1.28.0
Date: Mon, 02 Nov 2026 03:40:53 GMT
Content-Type: text/html
Content-Length: 492
Last-Modified: Mon, 11 Aug 2025 06:14:36 GMT
Connection: keep-alive
ETag: "68998a4c-1ec"
Accept-Ranges: bytes
<title>Papermerge DMS</title>
{"version":"3.5.3"}
HTTP=200 bytes=0
(/api/probe/ 返回 200 空 body,是官方健康检查端点;/api/version/ 回显 {"version":"3.5.3"}。)
4.6 登录验证
Papermerge 3.x 的登录走内置 auth-server 的 POST /api/token,必须 JSON 编码(form 编码实测 422):
curl -s -X POST http://127.0.0.1:8138/api/token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}'
实测回显(access_token 截前 260 字符,全文 729 字符):
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMGRiYTI5NC1hOTRmLTQ0NzktODhkZS1jOWRmMGFjNGMxN2YiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJzY29wZXMiOlsibm9kZS5jcmVhdGUiLCJub2RlLnZpZXciLCJub2RlLnVwZGF0ZSIsIm
TOKEN_LEN=729
响应同时通过 set-cookie: access_token=... 下发会话 Cookie(浏览器 UI 登录即此通道),JWT 的 sub 为用户 UUID。拿 token 后验证身份端点:
curl -s http://127.0.0.1:8138/api/users/me -H "Authorization: Bearer $TOKEN"
实测回显(原文照抄,scopes 数组为超级用户全量权限清单):
{"id":"a0dba294-a94f-4479-88de-c9df0ac4c17f","username":"admin","email":"admin@example.com","created_at":"2026-11-01T16:07:15.292229","updated_at":"2026-11-01T16:07:15.292229","home_folder_id":"6a1bdab1-a5ca-4475-be7e-627631ba8a02","inbox_folder_id":"a6e59095-e270-4f9a-9493-92f8a723d291","is_superuser":true,"is_active":true,"scopes":["custom_field.create","custom_field.delete","custom_field.update","custom_field.view","document.download","document.download","document.download.all_versions","document.download.last_version_only","document.update.custom_fields","document.update.document_type","document.update.tags","document.update.title","document.upload","document.upload","document_type.create","document_type.delete","document_type.select","document_type.update","document_type.view","group.create","group.create","group.delete","group.delete","group.select","group.update","group.update","group.view","group.view","node.create","node.create","node.delete","node.delete","node.move","node.move","node.update","node.update","node.view","node.view","ocrlang.view","ocrlang.view","page.delete","page.extract","page.extract","page.move","page.move","page.reorder","page.rotate","page.update","page.update","page.view","page.view","role.create","role.delete","role.select","role.update","role.view","shared_node.create","shared_node.delete","shared_node.update","shared_node.view","tag.create","tag.create","tag.delete","tag.delete","tag.select","tag.update","tag.update","tag.view","tag.view","task.ocr","task.ocr","user.create","user.create","user.delete","user.delete","user.me","user.me","user.select","user.update","user.update","user.view","user.view"]}
不带 token 访问受保护端点实测返回 HTTP=401,符合预期。UI 侧用浏览器打开 http://<服务器>:8138,以 admin / q1w2e3r4 登录即可。
4.7 上传、OCR 与全文检索闭环验证
以下序列全部走 REST API(与 UI 同一后端),命令与回显为实测照抄。
1) 建目录 finance(父节点取 users/me 的 inbox_folder_id):
curl -s -X POST http://127.0.0.1:8138/api/nodes/ \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"finance","ctype":"folder","parent_id":"a6e59095-e270-4f9a-9493-92f8a723d291"}'
实测回显(记录新节点 id 到变量 FID):
FID=48f098ee-8f86-4a24-ae84-bd11d4efdb8c
2) 建文档节点 pm-invoice(lang=deu、ocr=false 表示稍后手动调度 OCR):
curl -s -X POST http://127.0.0.1:8138/api/nodes/ \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"pm-invoice","ctype":"document","lang":"deu","ocr":false,"parent_id":"48f098ee-8f86-4a24-ae84-bd11d4efdb8c"}'
实测回显:
DOCID3=a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd
3) 上传扫描件(multipart):
curl -s -X POST http://127.0.0.1:8138/api/documents/$DOCID3/upload \
-H "Authorization: Bearer $TOKEN" -F "file=@invoice-scan.pdf"
实测回显(从响应 JSON 的 versions[0] 提取关键字段):
upload ok: size=81232 page_count=1 version_id=7371b12f-0f45-48e5-9e9b-2da9fa490d68
上传响应体是完整文档对象(含 versions/pages/download_url 等),原始全文示例见《Papermerge 3.5.3 API 使用详解》上传节。
4) 调度 OCR(deu):
curl -s -X POST http://127.0.0.1:8138/api/tasks/ocr \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"document_id":"a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd","lang":"deu"}'
实测回显:null(HTTP 200,任务已投递到 ocr 队列;受理即返回,异步执行)。
5) 等待 1-2 秒后验证 OCR 产物。 任务执行不打印 celery 日志,验证看两处:Redis 中的 celery-task-meta-*(实测全部 SUCCESS,单页 PDF 从投递到文本入库约 1-2s)与 media 盘上的产物:
find data/media/ocr -name page.txt | head -4
F=$(find data/media/ocr -name page.txt | head -1); echo "--- $F ---"; cat "$F"
data/media/ocr/pages/3a/65/3a6508af-1199-4100-8154-3f9ad27b7f23/page.txt
data/media/ocr/pages/9e/70/9e70675f-4504-4c8b-84ff-f78bc87bd08d/page.txt
data/media/ocr/pages/7a/30/7a30ce83-ae00-4bc6-b638-27387140c421/page.txt
data/media/ocr/pages/88/a6/88a68182-b667-4174-ac68-2e49c0f5847e/page.txt
--- data/media/ocr/pages/3a/65/3a6508af-1199-4100-8154-3f9ad27b7f23/page.txt ---
PAPERMERGE TEXTLAYER MARKER 778811 qual
OCR 完成后文档自动产生第二个版本(实测 GET /api/documents/$DOCID3/versions):
[{"id":"665be4ac-5fd7-48aa-80c8-94d390b36e89","number":2,"short_description":"With OCR text layer"},{"id":"7371b12f-0f45-48e5-9e9b-2da9fa490d68","number":1,"short_description":"Original"}]
6) 全文检索闭环(最终态实测)。 对扫描关键词与文本层关键词各搜一次:
curl -s "http://127.0.0.1:8138/api/search/?q=KAFFEEMUEHLE" -H "Authorization: Bearer $TOKEN"
curl -s "http://127.0.0.1:8138/api/search/?q=778811" -H "Authorization: Bearer $TOKEN"
{"page_size":10,"page_number":1,"num_pages":1,"items":[{"id":"88a68182-b667-4174-ac68-2e49c0f5847e","title":"invoice-scan","lang":"en","tags":[],"page_number":1,"document_id":"125122b8-baca-4533-a47b-e64bafa48204","entity_type":"document"},{"id":"9e70675f-4504-4c8b-84ff-f78bc87bd08d","title":"invoice-fresh","lang":"en","tags":[],"page_number":1,"document_id":"2a4692ef-d728-4c2d-924d-d8e23b91d305","entity_type":"document"},{"id":"7a30ce83-ae00-4bc6-b638-27387140c421","title":"pm-invoice","lang":"en","tags":[],"page_number":1,"document_id":"a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd","entity_type":"document"}]}
{"page_size":10,"page_number":1,"num_pages":1,"items":[{"id":"3a6508af-1199-4100-8154-3f9ad27b7f23","title":"notes-text","lang":"en","tags":[],"page_number":1,"document_id":"7b3e8499-fcdc-4e33-bd81-84d2a4ed3843","entity_type":"document"},{"id":"84585308-cead-4240-846b-fa97c800148b","title":"pm-notes","lang":"en","tags":[],"page_number":1,"document_id":"b55c1248-fb18-4f75-b41f-befbbc9e6243","entity_type":"document"}]}
KAFFEEMUEHLE(纯扫描件 OCR 出的词)命中 3 条、778811(PDF 自带文本层)命中 2 条——登录 → 上传 → OCR → 索引 → 检索 全链路闭环成立。
7) 下载带 OCR 文本层的最新版本:
VID=$(curl -s http://127.0.0.1:8138/api/documents/$DOCID3/last-version/ \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")
curl -s http://127.0.0.1:8138/api/document-versions/$VID/download \
-H "Authorization: Bearer $TOKEN" -o /tmp/pm-dl.pdf
echo "downloaded version=$VID bytes=$(stat -c %s /tmp/pm-dl.pdf) sha256=$(sha256sum /tmp/pm-dl.pdf | cut -c1-16)"
实测回显:
downloaded version=665be4ac-5fd7-48aa-80c8-94d390b36e89 bytes=50126 sha256=b400cab5c5160e3f
(OCR 版比原始 81232 字节小,因文本层替换了原图像编码——README 宣称的"OCR 文本叠加层可下载"实测成立。)
五、数据持久化说明
三个 bind mount 目录即全部状态,备份它们等于备份整个 Papermerge:
| 宿主目录 | 挂载点 | 内容 | 实测用量(闭环跑完) |
|---|---|---|---|
data/pgdata | db:/var/lib/postgresql/data | 元数据库(节点/文档/用户/标签/自定义字段) | 48M |
data/media | webapp 与 ocr_worker:/var/media/pmg | 上传原件 + OCR 产物 | 2.0M |
data/solr | solr:/var/solr | Solr 索引(core: pmg,含 conf 与段数据) | 1.9M |
media root 的盘上布局按 UUID 前 4 个 hex 字符两级分片:原始文件在 docvers/xx/yy/<文档版本UUID>/<原文件名>,OCR 逐页产物在 ocr/pages/xx/yy/<页UUID>/page.pdf + page.txt。find data/media -maxdepth 4 | head -20 实测回显:
data/media
data/media/ocr
data/media/ocr/pages
data/media/ocr/pages/3a
data/media/ocr/pages/3a/65
data/media/ocr/pages/9e
data/media/ocr/pages/9e/70
data/media/ocr/pages/7a
data/media/ocr/pages/7a/30
data/media/ocr/pages/88
data/media/ocr/pages/88/a6
data/media/ocr/pages/84
data/media/ocr/pages/84/58
data/media/ocr/pages/0c
data/media/ocr/pages/0c/85
data/media/docvers
data/media/docvers/00
data/media/docvers/00/08
data/media/docvers/00/08/00088034-7d72-4b34-b8bb-a5829115c297
data/media/docvers/5a
find data/media/docvers -type f | head -5 实测回显:
data/media/docvers/00/08/00088034-7d72-4b34-b8bb-a5829115c297/notes-text.pdf
data/media/docvers/5a/e8/5ae876fd-98b9-4041-9759-aa967be2bc83/invoice-scan.pdf
data/media/docvers/af/7d/af7d6852-b230-4d9a-80cd-81f24c8b5f63/notes-text.pdf
data/media/docvers/73/71/7371b12f-0f45-48e5-9e9b-2da9fa490d68/invoice-scan.pdf
data/media/docvers/73/93/7393775a-9768-4513-844b-7f380773d14b/notes-text.pdf
要点:上传文件不进数据库(官方文档明示,media root 独立持久化是官方 compose 教程专门一章的原因);data/solr 可以整目录删除——索引是数据库+media 的派生物,删掉后重建即可(见 8.3)。
六、环境变量表
compose 中实际使用的变量(默认值与更多参数详解见《Papermerge 3.5.3 配置参数详解》):
| 变量 | 本例取值 | 说明 |
|---|---|---|
| PAPERMERGE__SECURITY__SECRET_KEY | 64 位随机串 | 必填,JWT 签名密钥,泄露即可伪造登录态 |
| PAPERMERGE__AUTH__USERNAME | admin | 超级用户名,默认 admin |
| PAPERMERGE__AUTH__PASSWORD | q1w2e3r4 | 必填,超级用户密码,首启自动建号 |
| PAPERMERGE__AUTH__EMAIL | admin@example.com | 超级用户邮箱 |
| PAPERMERGE__DATABASE__URL | postgresql://pmuser:...@db:5432/pmgdb | 默认 SQLite;三组件必须一致 |
| PAPERMERGE__REDIS__URL | redis://redis:6379/0 | Celery broker;webapp 与 worker 必须一致 |
| PAPERMERGE__SEARCH__URL | solr://solr:8983/pmg | 默认 xapian:////index_db;webapp 与 i3worker 必须一致 |
| PAPERMERGE__MAIN__MEDIA_ROOT | /var/media/pmg | 默认 media;webapp 与 ocr_worker 共享 |
| PAPERMERGE__OCR__DEFAULT_LANG_CODE | deu | OCR 默认语言(仅 AUTOMATIC=false 时参与 UI) |
| PAPERMERGE__OCR__LANG_CODES | deu,eng,ron | 官方定位 UI 下拉展示;实测 3.5.3 无消费代码 |
| OCR_WORKER_ARGS | -Q ocr -c 2 | ocrworker 的 celery 参数(队列名固定 ocr) |
| I3_WORKER_ARGS | -Q i3 -c 2 | i3worker 的 celery 参数(队列名固定 i3) |
| POSTGRES_USER/PASSWORD/DB | pmuser/q1w2e3r4/pmgdb | db 服务初始化参数 |
命名约定:PAPERMERGE__<SECTION>__<KEY>,双下划线分节、全大写;每个组件(webapp/auth-server/ocrworker/i3worker)各自有一套 pydantic-settings 定义,同名键需在各服务间保持同步。
七、等价 docker run(最小单服务形态)
官方文档给出的快速演示形态是单服务 docker run(来源:docs.papermerge.io setup/docker,查询日期 2026-11-01,非本文实测):
docker run -p 12000:80 \
-e PAPERMERGE__SECURITY__SECRET_KEY=abc \
-e PAPERMERGE__AUTH__PASSWORD=pass123 \
papermerge/papermerge:3.5.3
访问 http://localhost:12000,凭据 admin / pass123。与本文 compose 全栈的差异:数据库落容器内 SQLite、检索为本地 Xapian、无 OCR/索引 worker,官方明确此形态仅作快速演示。用它先验证"能登录、能上传",再升级到本文六服务形态,是官方推荐的渐进路线。Apple 芯片需先 docker pull --platform linux/amd64 papermerge/papermerge:3.5.3。
八、常用运维命令
8.1 启停与状态
docker compose up -d # 启动/更新后拉起
docker compose down # 停止并移除服务(数据在 bind mount,不丢)
docker compose ps # 状态一览(见 4.5)
docker compose logs -f webapp # 追日志;worker 同理
8.2 健康检查与版本
curl -s http://127.0.0.1:8138/api/probe/ -H "Authorization: Bearer $TOKEN" # 200 即健康
curl -s http://127.0.0.1:8138/api/version/ -H "Authorization: Bearer $TOKEN" # {"version":"3.5.3"}
8.3 重建检索索引
Solr 里数据异常(如 schema 变更后、误删 core 内容后)的标准恢复流程,在 i3worker 内执行:
docker exec papermerge-353-stack-i3worker-1 sh -c 'cd /app && poetry run i3 apply'
docker exec papermerge-353-stack-i3worker-1 sh -c 'cd /app && poetry run i3 index --rebuild'
i3 apply 按 i3worker 自己的字段定义恢复 schema(含 copyField),--rebuild 全量重灌。实测重建后 Solr count=8,API 检索恢复命中(见 4.7)。切勿改用 webapp 侧的 paper-cli index-schema apply,两者模型不一致会毁掉索引写入(问题 7)。
8.4 Solr 直查(排障用)
Solr 未对外发布端口,从内部查:
docker exec papermerge-353-stack-solr-1 \
curl -s "http://127.0.0.1:8983/solr/pmg/select?q=KAFFEEMUEHLE&wt=json"
实测回显(修复后):numFound= 3,命中 doc id 与 API 搜索一致。
8.5 任务队列观测
ocrworker/i3worker 默认不打印 celery「Task received」日志(worker 日志只见启动横幅),验证任务是否真正执行要看 Redis 里的任务元数据键 celery-task-meta-*(redis-cli 连到 redis 服务的 6379 端口即可 KEYS/GET 查看)与 media 盘上产物。实测:闭环期间 Redis 中 18 个 celery-task-meta-* 键状态全 SUCCESS,date_done 与上传/OCR 操作时刻吻合(如 00:16:07-08 与 00:22:07-08 两批);单页 PDF 从投递到文本入库约 1-2 秒。
九、常见问题(排障实录)
以下 7 个问题全部来自本次部署实测踩坑,按暴露顺序记录。
问题 1:REST 路径带 /v1 一律 404
现象:按直觉写 POST /api/v1/tasks/ocr,webapp 日志 "POST /v1/tasks/ocr HTTP/1.0" 404,响应 {"detail":"Not Found"}。
根因:nginx 把 /api/(.*) 反代到 :8000/(.*),OpenAPI 实际路径没有 /v1 段(/api/token、/api/nodes/、/api/documents/{id}/upload、/api/tasks/ocr、/api/search/、/api/users/me、/api/version/、/api/probe/)。
处理:一律用 /api/*;端点清单以实例匿名可读的 GET /openapi.json(title "Papermerge DMS REST API" version 3.5.3)为准。
问题 2:登录接口 form 编码 422
现象:POST /api/token 用 application/x-www-form-urlencoded 返回 422。
处理:必须 JSON:-H 'Content-Type: application/json' -d '{"username":"admin","password":"q1w2e3r4"}'(见 4.6)。
问题 3:官方文档的 create_token.sh 在 3.5.3 镜像内是坏的
现象:官方 REST API token 文档说用 docker exec <webapp> create_token.sh <username> 生成 token;实测脚本执行报:
could not find task "auth-cli"
根因:脚本内容是 cd /auth_server_app && poetry run task auth-cli,而镜像内 auth-server 的 pyproject 没有注册该 task(list_users.sh 同病)。
处理:3.5.3 拿 API token 的唯一可用路线就是 POST /api/token 登录换 JWT(实测 token 729 字符、有效期 360 分钟)。
问题 4(核心):OCR 成功但全文检索恒 0 命中
现象:OCR 任务全 SUCCESS、media 上 page.txt 文本正确(RECHNUNG ... Stichwort: KAFFEEMUEHLE ...),但 GET /api/search/?q=KAFFEEMUEHLE items 空,Solr 直查也 numFound=0。
诊断链(逐条实测):
- Redis
celery-task-meta-*全 SUCCESS → 任务没失败; - 数据库 pages 表:OCR 生成页 text_len 40/122 → 文本已入库;
- Solr
q=*:*numFound=3,文档里有text_txt_en:"PAPERMERGE TEXTLAYER MARKER 778811 qual",q=text_txt_en:778811命中 1 → 索引其实写进去了; - Solr 访问日志显示 webapp 发的查询是裸词:
params={q=KAFFEEMUEHLE+AND+user_id:...}hits=0,而q=text:778811直接 400undefined field text; - core 的 solrconfig.xml 默认字段
df=_text_是启用态,但默认 managed-schema.xml 没有到_text_的 copyField:q=_text_:KAFFEEMUEHLEhits=0。
根因:salinic(Papermerge 检索组件库)按"裸词 + df=text"查询;官方 compose 用默认 configset 建核,默认 schema 无 *_txt_en → _text_ copyField → 全文检索永远 0 命中。官方文档示例本身带坑。
处理:core schema 增加 <copyField source="*_txt_en" dest="_text_"/>(title/text 都走 *_txt_en 动态规则),RELOAD core 后重新索引。推荐在首启前备好打过补丁的 configset(见 4.1)。
问题 5:上传后 i3worker 刷 NO OCR TEXT FOUND!
现象:i3worker 日志出现:
NO OCR TEXT FOUND! version=1 title=invoice-scan page.number=1
根因:上传即触发一次索引同步,此时 OCR 还没跑,原始页无文本属正常时序噪音;OCR 完成后链尾 i3 会再索引带文本的页。
处理:无需处理;若 OCR 完成后仍不命中,查问题 4/6/7。
问题 6:page_number 段类型冲突,且手改 schema 文件 RELOAD 不生效
现象:修好 copyField 后页面文档仍进不了 Solr,日志:
Exception writing document ... cannot change document field "page_number" from doc values type=SORTED_NUMERIC to inconsistent doc values type=NUMERIC
根因:salinic 把 page_number 按单值 NumericField 写入(NUMERIC docValues),而 core 里已按默认 schema 的多值定义写过旧段(SORTED_NUMERIC),段类型与 schema 不一致即拒写。两个坑中坑:
- bind mount 下直接编辑
data/solr/data/pmg/conf/managed-schema.xml后action=RELOAD不生效——RELOAD 后 Schema API 仍返回multiValued:true(Solr 以版本化 managed schema 为准)。正确做法走 Schema API:POST /solr/pmg/schema {"replace-field":{"name":"page_number","type":"pint","multiValued":false,...}}; - 只改 schema 不够,旧段还在:需
delete *:*+ commit +optimize(maxSegments=1)清旧段,本例还叠加了docker compose up -d --force-recreate solr才彻底清掉 core 缓存。
验证:单值写入 status=0;i3 index --rebuild EXIT=0,Solr count=8;API 搜索 KAFFEEMUEHLE=3、778811=2 命中(见 4.7 最终回显)。
问题 7(核心):webapp 的 index-schema apply 与 i3worker 冲突,跑错即毁索引写入
现象:首启 entrypoint 的 exec_index_schema_apply 因 Solr 未就绪失败(连接异常 traceback,但服务照常启动)。事后在 webapp 里补跑 paper-cli index-schema apply,返回码 0,此后 i3worker 写 Solr 全部被拒:
cannot change field "document_id" from index options=DOCS_AND_FREQS_AND_POSITIONS to inconsistent index options=NONE
根因:webapp 与 i3worker 各有一套 IndexEntity 字段定义,webapp 侧 apply 把 document_id 等字段改成了不索引/异类型;而 salinic 的 client.add/remove 不检查响应状态码——i3 index --rebuild 退出码 0 但 Solr count=0,静默失败。
处理:用 i3worker 自己的 i3 apply 恢复字段定义(含 copyFields)再 i3 index --rebuild(见 8.3)。教训:3.5.3 官方 compose 场景下 Solr core 归 i3worker 管,不要碰 webapp 的 index-schema apply。
其他小坑速查
| 现象 | 结论 |
|---|---|
POST /api/nodes/ 不带 parent_id | 422 Field required;父节点取 users/me 的 inbox_folder_id |
POST /api/users/ 缺 role_ids/group_ids | 422 两个 missing;补空数组即可 |
| ocrworker 启动 SecurityWarning(root 运行) | 官方镜像固有行为,可忽略 |
PAPERMERGE__OCR__LANG_CODES 配了没效果 | 3.5.3 实测无消费代码,OCR 语言真实白名单见《Papermerge 3.5.3 配置参数详解》 |
十、参考链接
- 官方 Docker Compose 部署文档:https://docs.papermerge.io/latest/setup/docker-compose/ (渐进式部署各形态 compose 原文,页脚 June 1, 2026,查询日期 2026-11-01)
- 官方 Docker 快速开始:https://docs.papermerge.io/latest/setup/docker/ (等价 docker run 最小形态出处)
- 官方版本矩阵:https://docs.papermerge.io/latest/admin-manual/version-matrix/ (3.5.3 对应 postgres:16.1/redis:7.2/solr:9.7/ocrworker:0.3.1/i3worker:0.3)
- GitHub 仓库:https://github.com/papermerge/papermerge-core
- 站内参考:《Papermerge 介绍与选型指南》《Papermerge 3.5.3 配置参数详解》《Papermerge 3.5.3 API 使用详解》