部署配方 · 文件与存储
Paperless-ngx:会回答问题的档案柜
被它取代的那套「扫描归档」流程,是一棵带着两个人各有意见的命名约定的目录树。Paperless-ngx 接手扫描件,做 OCR,猜出往来方和日期,并让整个库可以搜索——于是「锅炉保修卡在哪」从二十分钟的翻找变成十秒的检索。
| 容器镜像 | ghcr.io/paperless-ngx/paperless-ngx:2.14.5 |
|---|---|
| 宿主端口 | :8010/tcp |
| 数据目录 | /srv/homelab/paperless/media |
| 内存预留 | 900 MB |
| CPU 上限 | 1.0 vCPU (cpus: "1.0") — OCR is the only expensive operation and it is bursty |
| 更新节奏 | quarterly. The database migrations are well tested, but the OCR language packs and the classification model need re-downloading after a major bump |
| ARM 兼容 | arm64 native including the Tesseract OCR binaries; OCR on a Pi is slow but completely usable for household volumes |
| 宿主 | 容器 | proto | 暴露 | 用途 |
|---|---|---|---|---|
| :8010 | 8000 | tcp | 仅局域网 | web UI for search, tagging and the document viewer |
部署步骤
按容器期望的 uid 建好三个目录
USERMAP_UID 决定运行时用户,媒体和数据目录由它写入。投递目录在 NAS 上,那边也要做同样的处理。
run sudo mkdir -p /srv/homelab/paperless/{data,media,export} sudo chown -R 1000:1000 /srv/homelab/paperless sudo chown -R 1000:1000 /mnt/nas/paperless-consume首次启动时把 OCR 语言包装上
镜像会在首次启动时按 PAPERLESS_OCR_LANGUAGES 下载语言包。没装就启动,之后每一份非英文文档都会是空的。
run docker compose up -d paperless-db paperless-redis docker compose up -d paperless docker logs -f paperless | grep -i 'installing language' # 等它装完再上传任何东西建好账号并关闭自助注册
通过注册表单创建的第一个账号成为管理员。把家里人的账号加完,顺手把注册关掉。
run # 管理 -> 用户 -> 新建用户,然后 # 管理 -> 设置 -> 关闭「允许注册」在导入任何东西之前,先定好 OCR 语言和工作进程数
在一万份文档处理完之后再改 OCR 语言,意味着要全部重跑 OCR。先定下来。
run docker exec paperless document_ocr --help docker exec paperless document_index reindex --help # 万一设置还是错了,这两个命令都在先做一份测试文档,再做一次恢复演练
在把全家的档案托付给它之前,先导出并导入到一个临时实例里。发现「导出里并没有你以为的东西」这件事,现在做比在旧档案柜已经清空之后做要好。
run docker exec paperless document_exporter /usr/src/paperless/media/export --no-thumbnail du -sh /srv/homelab/paperless/media/export ls /srv/homelab/paperless/media/export | head
进件路径决定这套东西能不能用起来
Paperless 盯着一个投递目录。丢进去的任何东西都会被处理:OCR、分类、打标签,并按元数据改名。这个家里的做法是让扫描仪写到 NAS 上的一个文件夹,绝不直接写进 Paperless——这样扫描失败的表现是文件夹里躺着一个文件,而不是一个消失的任务。
第二个投递子目录用不同的命名约定,专门收那些应当算作来信而不是归档记录的文件。一开始就把投递结构定对,能省下以后给一千份文档重新打标签的工夫。
# consume/ -> 常规进件
# consume/inbox/ -> 信件和账单,标为来信
# 丢一个 PDF 进去看它处理
docker logs -f paperless | grep -i consuming树莓派上的 OCR:很慢,但是那种正确的慢
Tesseract 在 Pi 4 默认设置下大约每分钟二十五页。十页的水电账单二十秒;一百页的房贷文件四分钟,期间占满一个核。
工作进程数刻意设为一。四核板子上开两个 OCR 进程会让机架上其它一切都变迟钝,而且没有任何家庭文档量能证明第二个进程有必要。
| 文档 | 页数 | Pi 4 上耗时 | CPU |
|---|---|---|---|
| 水电账单 | 3 | 约 8 秒 | 一个核,短暂 |
| 保单 | 24 | 约 60 秒 | 一个核 |
| 房贷材料 | 120 | 约 5 分钟 | 一个核,持续 |
| 无文字的扫描照片 | 1 | 约 2 秒 | 一个核,短暂 |
文件命名模板与「家用级」元数据
文件名格式是一套基于元数据字段的模板,它在投递时应用,而不是在改名时应用。太晚才决定它,要么忍受不一致的文件名,要么触发整个档案重新处理。
日期是最容易出错的一个字段,因为 Paperless 是从正文里猜的。凡日期在法律上有意义的文档,都要打开把日期改正再让它进档案——因为错误的日期比缺失的日期更难在以后找出来。
PAPERLESS_FILENAME_FORMAT: '{created}-{correspondent}-{title}'
# -> 2026-03-14-Northwind Energy-Annual statement.pdf
# 改完之后跑:document_renamer导出才是真正的备份
只有媒体目录不够:没有数据库,标签、往来方、文档类型全都消失,只剩一堆命名正确的 PDF。反过来只有数据库,那就是一份列着不存在的文档的清单。
document_exporter 把两者加上元数据写成一棵可移植的目录树,未来的 Paperless 版本能重新导入。这里每月跑一次,叠在每晚的文件级复制之上,因为它是唯一能在整套栈彻底重建之后依然存活的东西。
docker exec paperless document_exporter /usr/src/paperless/media/export \
--no-thumbnail --use-filename-format
# 导出的树是自描述的,可以重新导入compose 文件
整份文件放在 /srv/homelab/paperless-ngx/compose.yaml。标签固定版本号,不用 latest——树莓派上回滚比升级麻烦得多。
services:
paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:2.14.5
container_name: paperless
restart: unless-stopped
ports:
- "8010:8000/tcp"
environment:
TZ: Asia/Shanghai
PAPERLESS_REDIS: redis://paperless-redis:6379
PAPERLESS_DBHOST: paperless-db
PAPERLESS_DBNAME: paperless
PAPERLESS_DBUSER: paperless
PAPERLESS_DBPASS: ${PAPERLESS_DB_PASSWORD}
PAPERLESS_OCR_LANGUAGES: eng chi_sim
PAPERLESS_OCR_LANGUAGE: eng+chi_sim
PAPERLESS_TIME_ZONE: Asia/Shanghai
PAPERLESS_URL: https://docs.lan
PAPERLESS_OCR_MODE: skip_noarchive
PAPERLESS_FILENAME_FORMAT: '{created}-{correspondent}-{title}'
PAPERLESS_TASK_WORKERS: "1"
PAPERLESS_THREADS_PER_WORKER: "1"
USERMAP_UID: "1000"
USERMAP_GID: "1000"
volumes:
- /srv/homelab/paperless/data:/usr/src/paperless/data
- /srv/homelab/paperless/media:/usr/src/paperless/media
- /mnt/nas/paperless-consume:/usr/src/paperless/consume
depends_on:
paperless-db:
condition: service_healthy
networks: [rack]
paperless-redis:
image: redis:7.4-alpine
container_name: paperless-redis
restart: unless-stopped
networks: [rack]
paperless-db:
image: postgres:16-alpine
container_name: paperless-db
restart: unless-stopped
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: ${PAPERLESS_DB_PASSWORD}
volumes:
- /srv/homelab/paperless/db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U paperless"]
interval: 10s
timeout: 5s
retries: 6
networks: [rack]
networks:
rack:
external: true加固清单
- 实例放在认证门户后面——一个可搜索的全家文档索引,能不用单因素就别用
- 账号按人分配,管理员账号只用于配置,普通会话改不了处理规则
- 投递目录是 NAS 上一个只有某个家庭账号可写的文件夹,也是扫描仪的进件通道
- OCR 语言集裁剪到实际使用的语言,既为速度,也因为没用的语言包就是多出来的、未经验证的解析代码
备份方案
有三样东西必须一起复制,分开就没意义了:存原始文件和 OCR 文本的媒体目录、数据库、以及投递目录。export 命令会写出一个包含全部内容的可移植归档,每月跑一次,作为双保险。
上线后验证
- 丢进去的 PDF 在一分钟内出现在档案里,且文本可搜索
- 搜索一个只存在于某页扫描图像里的短语能返回该文档,说明 OCR 跑了
- document_exporter 的输出能导入到临时实例,且标签结构一致
- 一批处理完之后投递目录是空的,失败项被挪进失败目录而不是无限循环
踩过的坑
- 一份没有 OCR 文字层、而 OCR 模式设置又不可读的 PDF,会产出一份没有文本、也没有报错的文档。在批量导入十年扫描件之前,先确认 OCR 模式。
- Postgres 和媒体目录必须一起备份。把媒体目录恢复到一份更旧的数据库上,会产生「作为文件存在、但作为记录不存在」的文档。
- 导入之后改文件名模板不会立刻改名,要跑 document_renamer;而跑它会改变路径,任何指向这些文件的外部链接就断了。
- 投递目录绝不能就是扫描仪直接写入的那个文件夹。一个只写了一半的文件被处理,会在档案里留下一份损坏的文档。
硬件与选型问答
- 树莓派做真实档案的 OCR 够快吗?
- 对家庭文档量来说够。一千份平均四页的文档,按每页两秒半算,大约两个半小时的后台 OCR。放夜里跑就结束了。派吃力的是批量导入五万页这种活——那应该用笔记本做,然后把数据目录搬过来。
- 扫得很糟的文档会怎样?
- 它们会带着很差的文本、但完整的原始图像进档案,所以你手工修正元数据之后依然能找到它们。重扫是可行的,但很少需要——更好的扫描仪和 300 dpi 的设置是预防问题,而不是事后修问题。
- 我可能要交给官方机构看的东西能放这儿吗?
- 能,导出目录树就是为此设计的:原件、启用对应模式时的 PDF/A 版本、以及 OCR 文本并排放着。法律事务上算数的是原件,这也是为什么进件流程从不修改源文件。