这是「Docker 好项目」系列的第 14 期。第 5 期讲了 Stirling-PDF,那是「临时处理一份 PDF」的工具;这一期解决另一件更长期的事:把手里的纸质票据、合同、保单、说明书变成一座能按内容搜索的档案库。

Paperless-ngx 效果图

痛点开场

先说一个几乎人人中招的场景。

一张三年前的车辆保险单,客户要用;一份半年前的维修结算单,财务要对账;一台家电的维修记录、一份上牌用的合格证照片——你确定它在手机里的某个相册里,但你不确定是哪个。于是你打开相册往上翻,翻到手指发酸。

云盘能解决"存"的问题,解决不了"找"的问题:它只能按文件名和文件夹找。而现实是——扫描出来的单据根本没有文件名,全是 IMG_20260912_0013.jpg 这种。你当初存的时候记得清清楚楚,三个月后连自己存过都忘了。

Paperless-ngx 解决的就是这一步:文件扔进"收件箱",它做 OCR、抽日期、抽金额、猜测是谁开给你的、按规则打标签、按你定义的路径重命名归档,然后把全文索引建起来。之后你只记得单据上有个词——比如"交强险""保养""年检"——搜这个词,那页纸就出来了。

它跟我自己那套工作流的关系是这样:星耀(我的飞牛 NAS)上跑着 Immich 管照片、Navidrome 管音乐,这台华为云 ECS 上跑着博客和 1Panel。照片是给人看的,文档是给人查的——这两件事的索引逻辑完全不同,所以它们值得各占一个服务。

能力边界

能干什么

  • 消费目录(consume)自动导入:任何丢进 consume/ 的文件都会被吃进去,支持 PDF、图片(jpg/png/tiff/…)和纯文本;Office 文档(Word/Excel/PPT)需要额外的 Tika + Gotenberg 容器
  • OCR 全文检索:基于 OCRmyPDF + Tesseract,加文字层之后连扫描件也能搜;v3 起搜索引擎换成 Tantivy(原来 Whoosh)
  • 元数据自动识别:日期、通讯录(correspondent,一般就是开票方)、文档类型、金额、标签;还有可训练的分类器,你标记几次之后它会自己学
  • 自定义字段:给单据加"合同编号""车牌号""金额"这类结构化字段,然后按字段筛选
  • 工作流(Workflow):新文件进来时按条件触发动作——打标签、设权限、改标题、发 Webhook。这一条是跟第 13 期 ntfy 对接的关键
  • 邮件收取:配好 IMAP 之后,它自己去邮箱里把带附件的邮件捞出来导入(发票自动归档的经典玩法)
  • 条码/ASN 分割:扫描时插入分隔页,一份 PDF 自动拆成多份;条码还能当标签注入(v3 起只剩 zxing-cpp 引擎)
  • 全文检索 + 保存视图:搜索语法、保存筛选条件、批量编辑
  • 多用户 + 细粒度权限:文档级、全局级权限;分享链接(可设过期)
  • 文件版本:v3 新增,同一份文档可以保留历史版本
  • AI 辅助建议(可选):接 OpenAI 兼容端点,让它帮你猜标题/标签,默认关闭

不干什么

  • 不扫描。它不会驱动你的扫描仪。要么用扫描仪自带软件扫完丢进 consume/,要么用手机扫描类 App 导出 PDF 再传上去
  • 不做同步盘。它管理的是"档案",不是你正在编辑的文件。想要双向同步请找 Syncthing / Nextcloud
  • 不修改原始文件。原始 PDF 它只读不改(OCR 结果是另存成 PDF/A 归档副本),想编辑 PDF 请回第 5 期 Stirling-PDF
  • 不替代记账/报销系统。它能存发票、能全文搜,但不会帮你出利润表
  • 不开箱即用。它是本篇里配置项最多的一期——但好消息是默认值基本够用,你要动的就那几个

一句话:Paperless-ngx 是你的文件柜,不是你的书桌。

部署前准备

  1. 一台能跑 Docker 的机器。官方最低要求不高,2 核 2G 能跑;但OCR 是纯 CPU 活,扫一份多页 PDF 能吃满一个核几十秒。华为云 ECS 上跟 1Panel 并排跑没问题,只要别同时压榨 CPU
  2. 规划域名,比如 docs.你的域名.com。这期强烈建议配域名 + HTTPS——它的分享链接、邮件里的链接、CSRF 校验全都依赖一个正确的对外地址
  3. 华为云安全组放行 80/443(给反向代理用)。8000 端口不要对公网开,映射成 127.0.0.1:8000 由反代转发
  4. 想清楚存储放哪。它有两块数据:数据库(元数据)+ media/ 里的原件。原件会长期增长,建议挂到大盘或 NAS 上,别跟系统盘挤
  5. 镜像。官方主推 ghcr.io/paperless-ngx/paperless-ngx,Docker Hub 上同一个项目叫 paperlessngx/paperless-ngx。

关于镜像 tag,这期我实测了一遍(v3.2.1 发布后):

tag 结果
3 404,不存在
3.2 200
3.2.1 200
latest 200

3.2、3.2.1、latest 三个 tag 的 manifest digest 完全相同(sha256:5fa76604a81d…),而且我在 ghcr.io 和 Docker Hub 两边都测了,digest 一致。再往上验历史:3.1 与 3.1.3 digest 相同、3.0 与 3.0.5 也相同——说明它的次版本 tag 确实会随补丁滚动(跟第 10 期 Gitea、第 13 期 ntfy 同类)。

所以这期推荐 :3.2:能吃到 3.2.x 的补丁,又不会被 3.3 的破坏性变更偷袭。别用 :latest——这个项目半年发了一大堆 minor 版本,latest 指向哪一期纯看运气。

⚠️ 如果你是从 v2 升级来的,v3 有不少破坏性变更(包括 PAPERLESS_SECRET_KEY 变成必填、数据库引擎必须显式写、OCR_MODE=skip 被移除等)。官方要求只能从 v2.20.15 升到 v3,细节我放在后面的踩坑清单里。

docker run 一行版

严格说这期没法"一行"——Paperless-ngx 必须有一个 Redis 兼容的 broker(celery 排任务用),所以最少是"两个容器"。下面这套用 SQLite 当数据库,适合先跑通看效果:

# 1) 建网络 + broker(官方 compose 现在用 valkey 代替 redis)
docker network create paperless

docker run -d \
  --name paperless-redis \
  --network paperless \
  --restart unless-stopped \
  valkey/valkey:9-alpine

# 2) 建目录
mkdir -p /opt/paperless/{data,media,consume,export}

# 3) 起 Paperless-ngx
docker run -d \
  --name paperless \
  --network paperless \
  -p 127.0.0.1:8000:8000 \
  -v /opt/paperless/data:/usr/src/paperless/data \
  -v /opt/paperless/media:/usr/src/paperless/media \
  -v /opt/paperless/consume:/usr/src/paperless/consume \
  -v /opt/paperless/export:/usr/src/paperless/export \
  -e PAPERLESS_REDIS=redis://paperless-redis:6379 \
  -e PAPERLESS_SECRET_KEY="$(openssl rand -base64 48)" \
  -e PAPERLESS_URL=https://docs.你的域名.com \
  -e PAPERLESS_TIME_ZONE=Asia/Shanghai \
  -e PAPERLESS_OCR_LANGUAGE=chi_sim \
  -e PAPERLESS_OCR_LANGUAGES=chi-sim \
  -e USERMAP_UID=1000 \
  -e USERMAP_GID=1000 \
  --restart unless-stopped \
  ghcr.io/paperless-ngx/paperless-ngx:3.2

几个必须知道的点:

  • PAPERLESS_SECRET_KEY 是硬性要求。v3 起不设、或者还留着示例值 change-me,容器会直接抛 ImproperlyConfigured 起不来(源码 settings/__init__.py 里写的)。上面用 openssl rand -base64 48 现场生成一个就行,注意把它记下来、别每次重启都换新的——它一换,所有登录会话和签名令牌全部失效。
  • PAPERLESS_URL 不做校验但极其重要。源码里它会被同时塞进 ALLOWED_HOSTS、CSRF_TRUSTED_ORIGINS、CORS_ALLOWED_ORIGINS。写错的话表现是"能打开、但一登录就 CSRF 报错",或者分享链接指向 localhost。不要带结尾斜杠。
  • 两行"中文 OCR"必须配对写,这是国内用户最容易翻车的地方:
  • PAPERLESS_OCR_LANGUAGES=chi-sim → 装的是 Debian 包 tesseract-ocr-chi-sim(连字符)
  • PAPERLESS_OCR_LANGUAGE=chi_sim → OCR 时用的语言代码(下划线)
  • 写反了不会报错,只会 OCR 出满屏乱码,或者启动日志里一行 Skipped tesseract-ocr-chi_sim: Package not found!
  • 额外语言包是容器启动时用 apt 现场装的——所以第一次启动要联网、要花几十秒。也正因为要 apt,它不能以非 root 用户运行(官方文档原文:"It is not possible to run the container rootless if additional languages are specified via PAPERLESS_OCR_LANGUAGES")。要中文 OCR,就别给容器加 user:。
  • 这里映射成 127.0.0.1:8000,是留给后面的反向代理——别把 8000 直接开到公网,这是个存着全部档案的系统。

docker-compose 推荐版

正式环境用这份(基于官方 docker/compose/docker-compose.postgres-tika.yml 改的,改动了三处:钉版本、加固配置、加中文 OCR):

services:
  broker:
    image: docker.io/valkey/valkey:9-alpine
    container_name: paperless-broker
    restart: unless-stopped
    volumes:
      - redisdata:/data

  db:
    image: docker.io/library/postgres:18
    container_name: paperless-db
    restart: unless-stopped
    volumes:
      # ⚠️ postgres:18 起数据目录改成 /var/lib/postgresql(PGDATA=/var/lib/postgresql/18/docker)
      #    老教程里的 /var/lib/postgresql/data 是 17 及以前的路径,照着抄会白挂一个空卷
      - pgdata:/var/lib/postgresql
    environment:
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: 换成你自己的强密码

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:3.2
    container_name: paperless
    restart: unless-stopped
    depends_on:
      - db
      - broker
      - gotenberg
      - tika
    ports:
      - "127.0.0.1:8000:8000"      # 只绑本机,由反向代理对外
    volumes:
      - ./data:/usr/src/paperless/data       # 数据库配置、索引、日志
      - ./media:/usr/src/paperless/media     # 原始文件 + 归档副本 + 缩略图(会越来越大)
      - ./consume:/usr/src/paperless/consume # 收件箱:往里丢文件
      - ./export:/usr/src/paperless/export   # 导出目录
    env_file: docker-compose.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBENGINE: postgresql     # v3 起必须显式写,不能再靠 DBHOST 推断
      PAPERLESS_DBHOST: db
      PAPERLESS_DBNAME: paperless
      PAPERLESS_DBUSER: paperless
      PAPERLESS_DBPASS: 换成你自己的强密码
      PAPERLESS_TIKA_ENABLED: 1
      PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
      PAPERLESS_TIKA_ENDPOINT: http://tika:9998

  # 下面两个只在需要导入 Word/Excel/PPT 时才要,纯 PDF + 图片场景可以整段删掉,省 500M 内存
  gotenberg:
    image: docker.io/gotenberg/gotenberg:8.37
    container_name: paperless-gotenberg
    restart: unless-stopped
    command:
      - "gotenberg"
      # 转 .eml 时禁用 JS、限制只能读 /tmp,避免跟踪像素和脚本
      - "--chromium-disable-javascript=true"
      - "--chromium-allow-list=file:///tmp/.*"

  tika:
    image: docker.io/apache/tika:3.3.1.0
    container_name: paperless-tika
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

配套的 docker-compose.env:

# 必填:v3 起不设就起不来。生成:python3 -c "import secrets; print(secrets.token_urlsafe(64))"
PAPERLESS_SECRET_KEY=换成你生成的64位随机串

# 对外地址(不带结尾斜杠),反代、分享链接、CSRF 全靠它
PAPERLESS_URL=https://docs.你的域名.com

# 时区,否则界面上的日期会按 UTC 算,看着像差了 8 小时
PAPERLESS_TIME_ZONE=Asia/Shanghai

# 中文 OCR:包名用连字符,语言代码用下划线,两个都要
PAPERLESS_OCR_LANGUAGES=chi-sim
PAPERLESS_OCR_LANGUAGE=chi_sim

# 让容器里的 paperless 用户对上宿主机的 UID/GID,避免 consume 目录没权限
USERMAP_UID=1000
USERMAP_GID=1000

# 首次启动自动建管理员(三个都写上才会建)
PAPERLESS_ADMIN_USER=admin
PAPERLESS_ADMIN_MAIL=你的邮箱@example.com
PAPERLESS_ADMIN_PASSWORD=换成强密码

几个「为什么这么写」:

  • PAPERLESS_DBENGINE 不能省。这是 v3 的破坏性变更:以前有 PAPERLESS_DBHOST 就自动认 Postgres,现在必须显式声明,否则它会安静地回落到 SQLite——你以为数据在 Postgres 里,其实在一个文件里。
  • PAPERLESS_URL 一旦填了,就不要再单独配 ALLOWED_HOSTS / CSRF_TRUSTED_ORIGINS。源码里它是"顺带把这三个都设上"的写法,填两处只会让排查变麻烦。
  • USERMAP_UID/GID:容器内默认用户是 uid 1000 的 paperless。宿主机上你的用户如果也是 1000,就不用手动 chown 挂载目录。注意这两个变量在非 root 启动时无效(v3 的启动脚本会打一行警告),跟前面说的"中文 OCR 别用 rootless"是同一个约束。
  • 健康检查镜像里自带:HEALTHCHECK --interval=30s CMD curl -fs -L http://localhost:8000,不用自己写。1Panel 的容器页能直接看到健康状态。
  • postgres:18 的卷路径别抄老教程。这个镜像从 18 开始把 VOLUME 从 /var/lib/postgresql/data 改成了 /var/lib/postgresql(PGDATA 变成 /var/lib/postgresql/18/docker)。照着旧文章写成 /data 子路径,后果是数据没落盘、重启即失忆。

配置项逐项解释

下面的默认值全部来自 v3.2.1 源码 src/paperless/settings/__init__.py 实测:

变量 默认值 说明 / 为什么这么写
PAPERLESS_SECRET_KEY 无(必填) 会话与签名令牌的密钥。v3 起留空或等于 change-me 直接启动失败
PAPERLESS_URL 空 对外地址。填了会同时写入 ALLOWED_HOSTS、CSRF_TRUSTED_ORIGINS、CORS_ALLOWED_ORIGINS,别配 trailing slash
PAPERLESS_ALLOWED_HOSTS * 默认全放。设成具体值时会自动加 localhost(健康检查要用)
PAPERLESS_TIME_ZONE UTC 改成 Asia/Shanghai。文档日期识别依赖它
PAPERLESS_DBENGINE 空(SQLite) 用 Postgres/MariaDB 时必填
PAPERLESS_REDIS 空 broker 地址。用官方 compose 时填 redis://broker:6379
PAPERLESS_TASK_WORKERS 1 celery 并发进程数。2C 机器保持 1;OCR 是 CPU 瓶颈,开多了只会互相抢
PAPERLESS_THREADS_PER_WORKER CPU核数 ÷ workers 默认已经是自动分配的合理值,别乱动
PAPERLESS_WORKER_TIMEOUT 1800 单个任务最长 30 分钟。超厚扫描件才需要调大
USERMAP_UID / USERMAP_GID 容器内 1000 让容器用户对应宿主机 UID,免掉 chown。非 root 启动时无效
PAPERLESS_ADMIN_USER 空(不自动建) 配合下面两个变量,首次启动自动建超管
PAPERLESS_ADMIN_MAIL root@localhost 超管邮箱,建议改成真实邮箱
PAPERLESS_ADMIN_PASSWORD 空 不设就不会建用户,只会在日志里提示
PAPERLESS_OCR_LANGUAGE eng OCR 语言代码。中文简体 = chi_sim(下划线),多语言用 eng+chi_sim
PAPERLESS_OCR_LANGUAGES 空(不装) 额外安装的语言包,用 Debian 包名的后缀。简体 = chi-sim(连字符)。镜像自带 eng/deu/fra/ita/spa
PAPERLESS_OCR_MODE auto auto=已有文字层就跳过,force=强制重跑,off=完全不做 OCR。⚠️ 老的 skip / skip_noarchive 已被移除
PAPERLESS_ARCHIVE_FILE_GENERATION auto 是否产出 PDF/A 归档副本,与 OCR 开关在 v3 已解耦。想恢复 v2 的"永远生成归档"就设 always
PAPERLESS_OCR_PAGES 不限 只 OCR 前 N 页。资料太多想省 CPU 时可以设
PAPERLESS_OCR_DESKEW true 自动纠偏。扫描歪了的单据靠它救回来
PAPERLESS_CONSUMER_POLLING_INTERVAL 0 0 = 用文件系统事件(即时)。放在 NFS/SMB 网络盘上必须改成正数(秒),否则它看不到新文件
PAPERLESS_CONSUMER_STABILITY_DELAY 5 文件大小/时间连续 5 秒不变才开吃。网络盘慢或扫描仪写入慢时调大
PAPERLESS_CONSUMER_RECURSIVE false 要不要递归扫子目录。按月份建文件夹的话就设 true
PAPERLESS_CONSUMER_IGNORE_PATTERNS [] 忽略规则。v3 起是正则(以前是 fnmatch),而且用户规则是追加到默认规则上,不是替换
PAPERLESS_CONSUMER_DELETE_DUPLICATES false v3 起默认不拒绝重复文件,改为在界面上提示。想恢复"重复就拒收"设 true
PAPERLESS_FILENAME_FORMAT 空 归档后的文件命名模板,例如 {{ created_year }}/{{ correspondent }}/{{ title }}。空 = 用内部 ID 命名
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE false 模板里解析不出来的变量直接省掉(不产生 none 目录名)。建议设 true
PAPERLESS_EMAIL_HOST / _PORT localhost / 25 只影响发邮件(任务失败通知等),不影响收邮件
PAPERLESS_EMAIL_TASK_CRON */10 * * * * 收邮件频率。没显式设置时它会按 SECRET_KEY 的哈希把自己的分钟错开,避免所有实例同一时刻打 mail server;disable 可关
PAPERLESS_TRAIN_TASK_CRON 5 */1 * * * 训练分类器。老 CPU(缺 SSE4.2)上要设 disable,否则 worker 会 SIGILL 崩
PAPERLESS_INDEX_TASK_CRON 0 0 * * * 每天 0 点优化索引
PAPERLESS_SANITY_TASK_CRON 30 0 * * sun 每周日 0:30 一致性自检
PAPERLESS_EMPTY_TRASH_TASK_CRON 0 1 * * * 每天 1 点清空废纸篓。注意:删掉的文件过了这个点就真没了
PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS true 默认允许 Webhook 打内网地址。锁死就设 false
PAPERLESS_TRUSTED_PROXIES 空 反代后的真实 IP 识别。配合 fail2ban 防爆破时建议设
PAPERLESS_WEBSERVER_WORKERS 1 Web 进程数,每个进程单独吃一份内存。2G 内存保持 1
PAPERLESS_AI_ENABLED NO AI 辅助建议,默认关闭,开了还要配 LLM 端点
PAPERLESS_POST_CONSUME_SCRIPT 空 导入成功后触发的脚本。v3 起不再传位置参数 $1..$8,改用 DOCUMENT_ID 等环境变量

1Panel 面板怎么搞

先说结论:1Panel 应用商店里没有 Paperless-ngx。我把 1Panel-dev/appstore 的 264 个应用翻了一遍,paperless 相关的命中是 0(这不奇怪,它需要 4~5 个容器协同,不适合做成"填两个参数就装好"的模板)。

所以在这台华为云 ECS 上,正确姿势是走 容器 → 编排(Compose):

  1. 1Panel 左侧 容器 → 编排 → 创建编排
  2. 名称填 paperless,把上面那份 docker-compose.yml 和 docker-compose.env 贴进去
  3. 1Panel 会把项目目录放在它自己的应用目录下(一般是 /opt/1panel/docker/compose/paperless),四个数据目录 data/ media/ consume/ export/ 都会生成在里面
  4. 点部署,等它把 5 个镜像拉下来——这一步在国内可能很慢,因为主镜像在 ghcr.io。拉不动的话在 1Panel 的 容器 → 配置 → 镜像加速 里配一个加速地址,或者临时把镜像换成 Docker Hub 的 paperlessngx/paperless-ngx:3.2(我实测两个 registry 的 digest 完全相同,是同一份东西)
  5. 起来之后在 1Panel 的 容器 页能看到 paperless 是 healthy

两个 1Panel 实操提示:

  • 别在 1Panel 里手改容器的端口映射。改了之后 PAPERLESS_URL 和实际的入口不一致,CSRF 会跟你翻脸。要改就改编排文件再重新部署。
  • 反向代理用 1Panel 的"网站"功能建:新建网站 → 反向代理 → 目标填 http://127.0.0.1:8000 → 申请 Let's Encrypt 证书。上传大 PDF 时记得把 Nginx 的 client_max_body_size 提到 100m 以上,否则几十兆的扫描件会 413。

首次访问

  1. 先确认容器活着、健康检查通过:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000
# 200(或 302 到登录页,两个都算正常)
  1. 浏览器打开 https://docs.你的域名.com
  2. 第一次访问它会直接引导你创建超级管理员账号(官方文档原话:you will be prompted to create a superuser account)。如果你在前面 .env 里写了 PAPERLESS_ADMIN_* 三个变量,那账号已经自动建好了,直接登录
  3. 登录后先去 Documents → 界面上传一份 PDF 试试,看能不能正常 OCR、能不能搜到里面的词
  4. 再验证收件箱:
# 往消费目录丢一份文件,等几秒
cp 某张单据.pdf /opt/1panel/docker/compose/paperless/consume/
# 看日志确认它在吃
docker logs -f paperless 2>&1 | grep -i "consume\|ocr"

搜索是这套系统真正的入口,官方文档里的搜索体验长这样——输入几个字母,文档、标签、通讯录、文档类型会分组返回:

Paperless-ngx 全局搜索预览

最小可用配置

"能跑"和"好用"之间,就差下面这几步。

1)先定一套归档命名规则,这是长期受益最大的一项:

PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=true

意思是:media/documents/2026/中国人保/车辆保险单.pdf。好处是——即使哪天 Paperless 挂了,你直接进 media/ 目录用系统自带的文件管理器也能找。这套系统最重要的设计就是"数据不锁死在应用里",别用默认的内部 ID 命名把这个优势浪费掉。

2)配好中文 OCR(前面已给,这里强调一次配对关系):

PAPERLESS_OCR_LANGUAGE=chi_sim
PAPERLESS_OCR_LANGUAGES=chi-sim

如果你的单据是中英混排(发票这种很常见),可以用 eng+chi_sim,两种语言同时识别,代价是慢一点。

3)把"收件箱"接到你顺手的地方。三种常见做法:

  • 扫描仪直接扫进 SMB/NFS 共享,再把共享挂进 consume/(记得同时改 PAPERLESS_CONSUMER_POLLING_INTERVAL,网络盘上事件通知不可靠)
  • 邮箱自动收:设置 → Mail → 新建 Mail Rule,填 IMAP 服务器和账号,指定"只处理来自某发件人/主题含发票的邮件"。它会按 PAPERLESS_EMAIL_TASK_CRON 的频率(默认约 10 分钟一次)去捞。QQ 邮箱、163 需要单独申请"IMAP 授权码",不是登录密码
  • 手机扫描 App 导出后上传:最土但最省事,界面上的上传按钮支持一次拖一堆

4)建标签体系,先少后多。经验是:标签只用来标"状态"(待报销/已归档/待付款),分类交给"文档类型"和"通讯录",这样后续的自动匹配和筛选会好用很多。

5)把权限理顺。如果你打算给同事用,第一步是创建一个日常普通账号,别天天用超管登录——超管能改全局配置、能删任何东西。文档级权限和全局权限在设置里是分开的两套,配工作流动作时也能顺手把"这批文档谁能看"一起定下来。

补一句正事:这套系统一旦上线,就等于把一堆纸质资料数字化集中到了一个地方。含客户个人信息(姓名、手机号)的扫描件慎入,或者至少单独建一批标签 + 只给特定用户权限,别和普通票据混在一起。

告警 / 通知配置

Paperless-ngx 跟第 1 期 Uptime Kuma、第 9 期 changedetection.io 不一样:它本身不是监控工具,所以这里的"告警"是两层意思——文档进来了要不要通知我,以及它自己挂了谁告诉我。

1)文档导入后发通知 → 接第 13 期的 ntfy

这是 Paperless 最漂亮的一个设计:工作流(Workflow) = 触发器 + 动作。触发器可以是"消费开始/文档更新/定时",动作可以是"打标签/设权限/改标题/发 Webhook"。

配法:设置 → Workflows → 新建:

  • 触发器:Consumption Started,过滤文件名 *.pdf(支持通配符)
  • 动作 1:Assignment —— 打上 Inbox 标签
  • 动作 2:Webhook —— 填 ntfy 的地址

Webhook 的目标 URL 就写你第 13 期搭的那条通道:

https://ntfy.你的域名.com/paperless

配一个 Authorization 头带上你的 tk_ 令牌即可(ntfy 支持 Bearer)。这样一份新单据进来的瞬间,手机上就响一下——比"隔三差五去后台看一眼"靠谱得多。

工作流编辑器长这样(官方截图,触发器和动作都是可叠加的):

Paperless-ngx 工作流编辑器

2)任务失败要能看到

左侧 Tasks 页是它的任务队列,OCR 失败、邮件抓取失败都会在这里留记录并标红。文件太大、格式不认识、密码保护 PDF 都会在这里报出来——新部署完第一周建议每天扫一眼。

想让它主动邮件告警,配好 SMTP 那几项即可:

PAPERLESS_EMAIL_HOST=smtp.example.com
PAPERLESS_EMAIL_PORT=465
PAPERLESS_EMAIL_HOST_USER=你的账号@example.com
PAPERLESS_EMAIL_HOST_PASSWORD=授权码
PAPERLESS_EMAIL_USE_SSL=true
PAPERLESS_EMAIL_FROM=你的账号@example.com

注意这里只管发邮件。收邮件(自动导入发票)是在界面里的 Mail Rule 单独配的,两套配置互不相干——这是新手最容易混的地方。

3)它自己挂了谁告诉你 → Uptime Kuma(第 1 期)

加一个 HTTP 监控,URL https://docs.你的域名.com,关键词可以填 Paperless(登录页标题里就有)。但告警通道别配 ntfy——如果故障刚好出在网络或 ntfy 那条链路上,你会收不到任何消息。这个坑第 13 期专门写过。

再加一个"磁盘快满"的检查:media/ 只会长大不会变小,超出磁盘的那一刻,数据库和 OCR 会一起崩。我自己的做法是用 Uptime Kuma 的 Push 监控 + 一个 cron 脚本上报磁盘占用。

反向代理与 HTTPS

以 Nginx Proxy Manager(第 2 期)为例:

  • Hostname/IP:127.0.0.1:8000(对上 compose 里绑的本机端口)
  • Scheme:http
  • 勾 SSL,申请 Let's Encrypt 证书
  • Advanced 里加上传体积限制,不然大 PDF 上传直接 413:
client_max_body_size 200m;
proxy_read_timeout 300s;

Caddy 用户更省事:

docs.你的域名.com {
    reverse_proxy 127.0.0.1:8000
    request_body {
        max_size 200MB
    }
}

三个反代相关的注意点:

  1. PAPERLESS_URL 必须和公网地址一字不差(含协议)。它是 CSRF 白名单的来源,写错的表现是"页面能开、登录按钮点了没反应/403"
  2. 登录报 403 的话,看这一条:v3 把登录限流的客户端 IP 判断改了(allauth),反代后面可能出现"认错 IP"导致误判。解法是加: ini PAPERLESS_TRUSTED_PROXIES=127.0.0.1 如果你的代理链不只一跳,还要用 PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT 指明跳数——注意它是"X-Forwarded-For 里的跳数",不是"配置了几个代理 IP"
  3. 反向代理终止 TLS 的场景,如果遇到"重定向到 http",再加一行(值是 JSON 数组字符串,格式不能改): ini PAPERLESS_PROXY_SSL_HEADER=["HTTP_X_FORWARDED_PROTO", "https"] 走 Cloudflare 的话,确认源站能收到 X-Forwarded-Proto;有异常先把这个子域名切成 DNS only(灰云)对比一下——我在 ntfy 那期用同样的方法定位过 CDN 层的问题

踩坑清单

  1. 不设 PAPERLESS_SECRET_KEY 直接起不来,而且不能填示例值 change-me。生成一次、写进配置文件、别每次重启重新生成,否则登录全掉。
  2. 中文 OCR 的两个值写反 = 静默失败:装包用 chi-sim(连字符),识别用 chi_sim(下划线)。日志里 Skipped tesseract-ocr-chi_sim: Package not found! 就是写错了。
  3. 中文 OCR + rootless 二选一。额外语言包是启动时用 apt 装的,非 root 装不了(官方文档明确不支持)。别一边设 user: 1000:1000 一边指望它装 chi-sim。
  4. PAPERLESS_DBENGINE 在 v3 是必填项。只用 PAPERLESS_DBHOST 的话它不会报错,会安静地退回 SQLite——数据就在容器挂载的 data/ 里,你以为在 Postgres。
  5. OCR_MODE=skip / skip_noarchive 已删除。v3 把"要不要 OCR"和"要不要生成归档"拆成了 OCR_MODE(auto/force/redo/off)和 ARCHIVE_FILE_GENERATION(auto/always/never)。老配置留着不生效,只会打一行 warning。升级前如果你依赖 v2 的"永远生成归档",必须显式设 always。
  6. 重复文件默认不再拒收了。v3 改成"允许重复、界面上提示"。要恢复旧行为设 PAPERLESS_CONSUMER_DELETE_DUPLICATES=true。
  7. 搜索索引从 Whoosh 换成了 Tantivy,格式不兼容,首次启动会自动全量重建——索引大的库第一次升级会卡一阵,属正常。另外查询语法变了:note:xx 要写成 notes.note:xx,custom_field:xx 要写成 custom_fields.value:xx;不带前缀的裸词搜索不再匹配笔记和自定义字段。
  8. 文档加密功能被移除了(v3)。如果你是从古早版本一路升上来的、且用过加密,必须先用 decrypt_documents 解密再升级,否则升完打不开。
  9. 条码引擎只剩 zxing-cpp,CONSUMER_BARCODE_SCANNER 这个变量已删除;如果你的自定义镜像里还装着 libzbar0,可以删了。
  10. 升级 v3 会清空任务历史。Tasks 页升级后是空的,不是丢数据。
  11. postgres:18 的卷路径变了:挂 /var/lib/postgresql(不是 /var/lib/postgresql/data)。照旧文章抄会得到一个"能启动、但每次重启数据都没了"的诡异现象。
  12. 网络盘上的收件箱看不到新文件:CONSUMER_POLLING_INTERVAL 默认 0 表示用文件系统事件,NFS/SMB 上事件通知经常不工作,必须改成秒数(比如 60)。
  13. consume/ 里的文件会被移走,不是复制。丢进去之后原件会进 media/,收件箱里就没了。别把它当成"长期存放目录"。
  14. 老 CPU 上的 trap invalid opcode:NumPy 2.4 起要求 CPU 支持 SSE4.2(grep -o -m1 sse4_2 /proc/cpuinfo 没输出就中招),表现是 worker 反复崩溃重启。官方给的出路是 PAPERLESS_TRAIN_TASK_CRON=disable(关掉分类器训练,规则匹配不受影响)。
  15. 上传大文件 413:不是 Paperless 的问题,是反代的 client_max_body_size 太小。
  16. v3 起 pre/post 消费脚本收不到位置参数了,$1~$8 全部改成环境变量(DOCUMENT_ID、DOCUMENT_SOURCE_PATH、DOCUMENT_THUMBNAIL_PATH 等)。老脚本会静默拿到空值。
  17. 别用 :latest。这个项目 minor 版本迭代很快,latest 随时可能跨版本跳,破坏性变更全砸你脸上。

备份

Paperless-ngx 的数据分三块,备份策略也不同:

位置 内容 重要性
media/ 原始文件 + OCR 归档副本 + 缩略图 最高,这份没了就真没了
Postgres 卷(或 data/db.sqlite3) 元数据:标签、通讯录、权限、自定义字段 高,丢了要重标一遍
data/ 索引、日志、配置缓存 低,可重建

官方推荐用内置导出命令,好处是导出的结构在导入时能被完整还原(包括标签、通讯录、自定义字段):

# 全量导出(文档 + 元数据)
docker exec -t paperless document_exporter ../export \
  --use-filename-format --delete

# 恢复(新机器上:先起容器,再执行)
docker exec -t paperless document_importer ../export

顺手的日常备份(media 用 rsync 增量、数据库用 dump):

#!/usr/bin/env bash
set -euo pipefail
BASE=/opt/1panel/docker/compose/paperless
DEST=/backup/paperless/$(date +%F)
mkdir -p "$DEST"

# 1) 数据库逻辑备份(最稳,跨版本也能恢复)
docker exec paperless-db pg_dump -U paperless paperless | gzip > "$DEST/paperless-db.sql.gz"

# 2) 文件增量同步(原件是命根子,且它就是普通文件)
rsync -a --delete "$BASE/media/" "$BASE/export/"

# 3) 顺手备份配置文件
cp "$BASE/docker-compose.yml" "$BASE/docker-compose.env" "$DEST/"

提醒一句:别在容器运行中直接 tar 打包 Postgres 的卷,可能拷到一个写了一半的文件,恢复时各种诡异报错。数据库永远优先用 pg_dump。

另外 PAPERLESS_EMPTY_TRASH_TASK_CRON 默认每天 1 点清空废纸篓——"删了还能救"的窗口最多一天,删重要单据前想清楚。

小结

适合谁

  • 家里一堆纸质单据、保单、说明书、合同,已经被"找不到"折磨过的人
  • 想给公司/店里做一个内部资料库,需要按内容搜、按权限分(4S 店场景下:整备单、上牌资料、保险单、供应商合同各归各类)
  • 已经在自托管一套服务(Uptime Kuma + NPM + Immich + ntfy…),愿意再加一个"资料库"补齐最后一块
  • 手上有扫描仪,或者愿意用手机扫描 App 的人

不适合谁

  • 只有两三份文件的——用第 5 期的 Stirling-PDF 加个网盘文件夹就够了,没必要上这套
  • 想要"手机拍一张、五秒后就出现在云端"的那种爽感的——Paperless 的 OCR 是有延迟的,它不是相册
  • 想拿它当在线协作编辑器的——它不是 Google Docs
  • 完全不想碰配置文件的——这期是所有期里配置项最多的一期,虽然默认值能跑,但域名、密钥、中文 OCR 这几项至少要进一次终端

我这台华为云 ECS 上,前面 13 期攒下来的服务已经有点"各自为政"的意思了:Uptime Kuma 看服务、Immich 管照片、ntfy 送通知、Miniflux 收 RSS。Paperless-ngx 补的是"信息入口"这一环——进来的纸、进来的发票、进来的合同,全都有一个能搜的地方。它可能是这个系列里最不"炫"的一期,但大概率是日子越久越值钱的那一个。

项目地址

  • GitHub:https://github.com/paperless-ngx/paperless-ngx
  • 官方文档:https://docs.paperless-ngx.com/(配置项全集在 configuration 页;v2 升级一定要先读 migration-v3)
  • 演示站:文档首页有 demo 链接,可以先点进去摸一遍再决定要不要装
  • Docker 镜像:ghcr.io/paperless-ngx/paperless-ngx:3.2,Docker Hub 同名项目为 paperlessngx/paperless-ngx:3.2(两者 digest 一致)