如果你跟我一样办公桌上永远堆着没空整理的发票、合同、说明书网盘里还散落着几百个命名混乱的扫描件那么建议你花点时间了解一下 paperless-ngx 这套开源的文档管理系统。它是 Paperless 项目的下一代版本核心能力就一句话把纸质文件和散乱的 PDF 统一归档、自动识别文字、打上标签然后你随时能在网页里搜到想要的那一份。适合所有被纸质文件淹没的个人用户、小团队也适合想给自己建一套长期知识库的人。我最早接触 paperless 还是老版本后来迁移到 paperless-ngx发现它在界面、权限、文档处理链路上都补了不少硬货。这篇文章不打算念官方文档我直接按自己的实操顺序来写先讲清楚它到底解决了什么痛点、内部是怎么运作的再给你一套能直接抄的 Docker Compose 部署方案最后把我踩过的坑和排查方法一次说清。建议大家先把文章过一遍再决定要不要动手因为部署本身不复杂但前期的目录规划和参数选择才是决定你半年后会不会后悔的关键。1. 整体思路paperless-ngx 到底解决什么问题1.1 纸质文档和零散文件的核心痛点我见过很多人对文档管理的理解就是“建一堆文件夹把扫描件扔进去”。这个做法在文件少的时候没问题文件一旦超过两三百份问题就全部暴露出来你要找一份两年前某银行的回单得先回忆当时把它放在了“银行回单”还是“2023发票”还是“其他杂项”里找到文件夹后里面经常是 30 个名为「scan_001.pdf」的同款文件只能一个个打开看缩略图。paperless-ngx 的思路完全不同。它不强迫你按目录结构归档而是让系统自己读取文档内容——扫描件也好、电子发票也好进去之后先做 OCR 识别成文字再结合你定义的对应方correspondent、文档类型document type、标签tag在数据库里建立索引。你要找东西的时候不需要知道它放在哪个“文件夹”只需要回忆一个关键词、一个时间范围或一个标签就能把它捞出来。这个体验有点像给纸质文件上了一套搜索引擎而且是本地私有化的。1.2 为什么选 paperless-ngx 而不是其他方案市面上的方案有商业的 Evernote、OneNote也有开源的 Nextcloud、Calibre Web为什么我最终选了 paperless-ngx而且从老版本一路跟过来几个实际理由第一它把“输入”这件事做得很顺。你不需要手动填表格、填元数据只要把文件丢进一个消费目录或者发一封带附件的邮件系统就自动完成 OCR、分类、归档。其他很多方案做不到这种“零维护”的摄入体验。第二它对扫描件、拍照件的识别效果好。底层用的是 OCRmyPDF 和 Tesseract中文识别率在线而且会自动生成带文字层的 PDF。即使你存进去的是纯图片paperless-ngx 也能把它转成可搜索的 PDF。这对国内用户非常重要因为很多发票、合同都是拍照件。第三部署足够轻量。官方推荐 Docker Compose 方式一个服务器或者一台旧电脑几行配置就能跑起来。数据都在你自己手里没有平台迁移问题。相比于商业笔记软件你不用担心中间商停服或者隐私数据外泄。如果你纠结 paperless-ngx 和老版本 paperless-ng 的差别我建议直接上新版本。它的前端从旧框架换成了新架构交互体验和搜索响应都好很多数据迁移也提供了现成脚本从老版本升级过去并没有想象中麻烦。2. 系统架构与数据处理链路2.1 核心组件与分工要驾驭 paperless-ngx第一步得先理解它的容器组成。官方 docker-compose 里通常会有这几个角色容器名职责说明webserverWeb 管理界面和 API用户日常打交道的入口也是绝大多数配置生效的地方consumer文档消费与处理监听 consume 目录执行 OCR、归档、索引的流水线dbPostgreSQL 数据库存储元数据、标签、对应方、全文索引的辅助数据redis缓存与任务队列给 consumer 和 webserver 做中间通信也用于并发任务调度gotenbergPDF 处理工具渲染文档预览、转换 PDF 格式是预览功能的重要依赖tika内容检测与元数据提取负责从各种文件类型里抽取文本信息辅助 OCR 结果整合这套架构说白了就是一个前后端分离再加一个异步任务系统。你通过网页上传文档时webserver 收到文件后把它写入 consume 目录consumer 容器感知到新文件后开始跑 OCR 和内容分析处理完的数据进入 PostgreSQL文件本体放在指定数据卷里。搜索的时候webserver 直接从数据库检索并返回结果。这样的好处是重活都在后台异步完成界面不会卡死你一次性塞进去 50 份文件也没问题consumer 会按顺序排队处理。2.2 一条文档从进来到被搜索经历了什么以一张扫描版合同为例完整链路是这样的你把scan.pdf放到documents/consume目录或者通过网页上传consumer 容器检测到新文件做格式初检tika 抽取原始文本如果发现是纯图片或者扫描件则调用 OCRmyPDF 和 Tesseract 进行文字识别系统根据预设规则自动分配对应方、文档类型、标签这一步是可选的基于你配置的匹配规则原始文件被转成带文字层的标准 PDF保存到归档目录PostgreSQL 写入元数据和全文索引消费完成后原文件从 consume 目录移走网页界面立即出现这篇文档你在搜索框里输入合同里的任何一个关键词就能定位到这份文件。这套链路最妙的地方在于“归档”不是终点而是起点。正因为有全文索引文件一旦入库你基本不再需要关心它存放在哪个物理路径也不用刻意建分类目录。这也是为什么我反复强调目录规划不要过于复杂把标签和搜索用起来效率会高得多。3. Docker 部署实操从零搭起来3.1 docker-compose 配置详解部署部分我直接给你一份我目前正在用的配置模板。它不是官网原版而是我根据实际服务器环境调整过的去掉了部分用不到的变量补上了中文 OCR 和时区设置。先说明下前置条件一台装有 Docker 和 Docker Compose 的 Linux 服务器2核4G内存就够跑了磁盘建议预留至少 50GB因为文档和数据库会持续增长。version: 3.4 services: broker: image: docker.io/library/redis:7 restart: unless-stopped volumes: - redisdata:/data db: image: docker.io/library/postgres:15 restart: unless-stopped volumes: - dbdata:/var/lib/postgresql/data environment: POSTGRES_DB: paperless POSTGRES_USER: paperless POSTGRES_PASSWORD: paperless webserver: image: ghcr.io/paperless-ngx/paperless-ngx:latest restart: unless-stopped depends_on: - db - broker - gotenberg - tika ports: - 8000:8000 volumes: - ./data:/usr/src/paperless/data - ./media:/usr/src/paperless/media - ./export:/usr/src/paperless/export - ./consume:/usr/src/paperless/consume environment: PAPERLESS_REDIS: redis://broker:6379 PAPERLESS_DBHOST: db PAPERLESS_DBUSER: paperless PAPERLESS_DBPASS: paperless PAPERLESS_DBNAME: paperless PAPERLESS_TIME_ZONE: Asia/Shanghai PAPERLESS_OCR_LANGUAGE: chi_simeng PAPERLESS_SECRET_KEY: change-me-to-a-long-random-string PAPERLESS_URL: http://192.168.1.100:8000 PAPERLESS_CONSUME_DUPLICATES: true PAPERLESS_FILENAME_FORMAT: {{ created_year }}/{{ correspondent }}/{{ title }} gotenberg: image: docker.io/gotenberg/gotenberg:7.10 restart: unless-stopped command: - gotenberg - --chromium-disable-javascripttrue - --chromium-allow-listfile:///tmp/* tika: image: ghcr.io/paperless-ngx/tika:latest restart: unless-stopped volumes: data: {} dbdata: {} redisdata: {}这里重点说几个参数因为很多人配置完跑不起来多半是这里理解偏了PAPERLESS_REDIS必须写成redis://broker:6379这里面的broker是 compose 里的服务名不是你的 IP 地址。容器之间通过 Docker 内部网络通信和宿主机没有关系。PAPERLESS_DBHOST同理写db而不是localhost。PAPERLESS_OCR_LANGUAGE我设置为chi_simeng这样中英文混排的文档都能识别。如果你只需要中文可以只写chi_sim但是国内文档常见中文夹杂英文数字建议保留eng。PAPERLESS_SECRET_KEY是 Django 的签名密钥一定不要用默认值改成一段足够长的随机字符串。这个只影响登录 session 和 API 令牌不涉及真正的数据加密但用弱密钥总有隐患。PAPERLESS_URL决定了网页里的分享链接、下载链接前缀如果你要配反向代理这里要改成你的公网域名或 IP不然点击某些按钮会跳到 localhost。PAPERLESS_CONSUME_DUPLICATES设置为 true 的意思是即使系统检测到重复文件也照样收下并归档。这个如果你希望系统自动去重可以保持 false。我建议打开因为真实场景里很多重复文件其实内容类似但不完全相同自动去重误杀的概率不低。3.2 启动前必须想好的几个参数在敲docker compose up -d之前有三件事最好提前定下来不然后期改起来很折腾。第一是目录映射。上面配置里我使用了./data、./media、./export、./consume四个相对目录分别对应数据库文件、媒体文件、导出备份目录、消费目录。实际生产环境我建议把consume单独拿出来映射成一个显眼的路径比如/home/yourname/paperless-consume这样你平时扫码或者手机传文件的时候直接丢到这个目录就行不需要每次进容器看路径。第二是文件命名规则。PAPERLESS_FILENAME_FORMAT控制归档后的文件名格式我用的是“年份/对应方/标题”三层结构。有人喜欢{{ created_year }}/{{ correspondent }}/{{ created_month }}/{{ title }}也可以但我不建议太深因为这个路径是在后台自动生成的层级太深反而会让导出的备份目录混乱。关键是{{ title }}默认是文档第一行文字如果识别率不佳会生成一长串奇怪字符所以建议配一个PAPERLESS_FILENAME_FORMAT时别把{{ title }}放在最前面避免文件名前几字是乱码。第三是反向代理。如果你不想每次都 IP:8000 这样访问而是想配一个域名和 HTTPS需要提前规划好。paperless-ngx 本身支持通过环境变量配置信任代理比如PAPERLESS_TRUSTED_PROXIES设置为 Nginx 容器 IP然后在 Nginx 里做proxy_pass http://你的服务器IP:8000。但这一步和 paperless-ngx 本身无关容易出问题建议新手先直接用 IP 访问跑通以后再上 HTTPS。3.3 首次登录与中文设置配置写好后执行docker compose up -d等几十秒让容器初始化数据库。首次启动会在 data 目录里自动创建数据库并且创建默认管理员账号。你可以用docker compose exec webserver createsuperuser命令创建管理员按提示输入用户名、邮箱、密码即可。登录网页后在右上角「设置」里可以把语言切换为中文或者在环境变量里写PAPERLESS_LANGUAGES界面会立即变成中文。这一步不做也不影响使用但中文界面确实能减少不少翻设置的时间。有一点要提醒很多人第一次启动后发现网页打不开多数不是 paperless-ngx 自身问题而是服务器防火墙没放行 8000 端口或者云平台的安全组规则没加。先在本机用curl http://localhost:8000测一下如果通那就是防火墙问题按各自的系统方式放行即可。4. 日常配置与深度使用4.1 OCR 参数调优从能用到好用系统默认的 OCR 配置对普通文档够用但如果你像我一样需要处理大量手机拍照件、传真件、字迹浅的合同建议做几个微调。首先是分辨率与图像预处理。paperless-ngx 的环境变量里有PAPERLESS_OCR_IMAGE_DPI和PAPERLESS_OCR_CLEAN。PAPERLESS_OCR_CLEAN设为true会启用图像清理对扫描件去噪、纠偏实测对倾斜的拍照文字有明显改善。但要注意图像清理会稍微增加处理时间一次性导入几百份时能感觉到排队变久。其次是 OCR 语言包的维护。如果PAPERLESS_OCR_LANGUAGE里面写了chi_simeng容器启动时会检查 Tesseract 的语言包是否存在不存在会自动下载。但网络不好时容易失败表现为日志里报语言包相关错误。这种情况下可以手动进入容器安装对应语言包或者直接把容器替换成一个带语言包的镜像。不过我实测下来官方镜像的自动下载在多数网络环境下都能成功只有公网带宽很差的机器会超时。还有一个重要参数是PAPERLESS_OCR_MODE。默认是redo意思是即使文档本身已经有文字层也强制重新 OCR 一遍如果改成skip遇到已有文字层的 PDF 就直接跳过 OCR。我个人的建议是保持redo因为很多 PDF 虽然带有文字层但字库不全或用特殊字体渲染提取出来是一堆乱码重新 OCR 反而能得到干净文本。代价是处理时间变长就看你怎么权衡了。4.2 标签、对应方与文件命名规则paperless-ngx 有三个核心元数据概念对应方Correspondent、文档类型Document Type、标签Tag。对应方就是这份文件的来源或归属方比如「XX银行」「XX物业」文档类型就是文件性质比如「发票」「合同」「说明书」标签更像用户的自由分类比如「待报销」「保修期内」。首次导入大量旧文件时手工逐个填元数据是最耗时的。我的做法是利用系统自带的「处理规则」Workflow来自动分配新建一个处理规则匹配条件设为“文件名包含 invoice”动作是设置文档类型为发票、对应方为某公司再建一个规则匹配“文本内容包含 劳动合同”动作是打上「合同」标签。这样只要消费目录里的文件满足条件归档时会自动完成分类。这个规则在「管理界面」里的「处理规则」模块配置条件支持文件名、标题、文本内容等多种维度逻辑还算灵活。文件命名规则我觉得是 paperless-ngx 最容易被忽略但很值得花时间调的地方。默认情况下归档文件会存成类似2024-05-01 发票扫描件.pdf这样的名字看多了完全没有辨识度。我用的格式是{{ created_year }}/{{ correspondent }}/{{ title }}这样归档目录结构非常清晰第一层年份第二层对应方第三层是文档标题。即使三年后我直接去文件系统翻也能快速定位。注意标题字段如果识别为空paperless 会默认用日期生成一个名字不会报错可以放心。4.3 邮件消费让文档自己进来这个功能是我个人最常夸的。你可以在配置里开启邮件消费给 paperless-ngx 配一个专用的邮箱账号它定期去收件箱拉取带附件的邮件附件自动进入消费流程。国内常用邮箱的 IMAP 设置基本都能配通只需要填服务器地址、端口、账号密码。配置路径在「管理界面」→「邮件账户」按提示填写 IMAP 服务器、账号、密码然后在「邮件规则」里设定哪些邮件、哪些附件要被消费。我通常会设一个独立的 QQ 邮箱或 Outlook 邮箱专门干这件事手机里装个邮件 App需要归档的附件发给这个邮箱即可。好处是显而易见的你不需要打开电脑、不需要进网页手机随手转发一封邮件paperless-ngx 自动收取、识别、归档。月底报销的时候我在网页里搜「报销」标签所有电子发票和扫描件一次全出来体验非常好。邮件消费里有一个细节要提醒系统默认只处理未读邮件处理完不会删除邮件只是标记已读。如果你怕邮箱越积越多可以在邮件规则里开启“删除邮件”选项但我不建议这样做万一系统误判邮件丢了没法找回。保留已读邮件定期手动清理安全得多。5. 备份、安全与长期维护5.1 备份方案对比千万别只复制文件夹很多人以为备份就是压缩一下挂载的 data、media 目录其实这样备份出来是残废的。paperless-ngx 的文档内容和数据库、全文索引是分开存的如果你只拷贝了 data 目录但没备份 PostgreSQL一旦数据库损坏你只能拿到一堆匿名 PDF元数据和搜索索引全部丢失。反过来只备份数据库也会丢原始文件。官方推荐的备份方式有两种方式优点缺点适用场景官方 Document Exporter导出元数据和文件结构清晰可用document_importer恢复导出频率需手动或脚本控制恢复时要跑导入流程关注文档可迁移性的场景整机/磁盘快照操作简单恢复快备份文件体积大且数据库和文件系统需一致快照服务器环境可控的小团队我目前的方案是 3 个层面同时做每月跑一次官方导出把导出目录同步到另一台机器的共享存储每周做一次 PostgreSQL 逻辑备份docker compose exec db pg_dump -U paperless paperless backup.sql每天用 rsync 把 data、media、consume 三块目录同步到备份服务器并保留最近 7 天版本。重点是搞清楚一件事只有把所有部分备份齐全并且做过至少一次“从备份的服务器完整恢复”的演练你才能说自己的数据是安全的。很多人备份脚本写了几年真正要恢复时发现某个路径错了或者数据库版本不匹配那时候后悔就晚了。5.2 数据库膨胀与全文索引维护用过一段时间以后你可能会发现 PostgreSQL 的数据目录越来越大。这主要是全文索引和文档历史版本造成的。paperless-ngx 每次重新保存文档都会生成新的数据记录长期更新文件后数据库里堆积了大量旧版本。这个不会在界面上直接体现但会显著拖慢搜索和备份速度。解决方法是定期做一次 VACUUMdocker compose exec db psql -U paperless -d paperless -c VACUUM (ANALYZE, VERBOSE);这个命令能回收空闲空间并更新统计信息。如果你的数据库实在太大也可以用VACUUM FULL但这个操作会锁表建议在半夜低峰期执行。全文索引方面paperless-ngx 每隔一段时间会自动运行索引优化任务但如果你改了检索语言或添加了很多文档后感觉搜索变慢可以手动触发docker compose exec webserver document_index rebuild_index这个命令会重新构建全文索引耗时视文档数量而定我跑过一次一万多份的索引大概需要几分钟到十几分钟不等。重建期间搜索可能会短暂返回不完整结果建议维护窗口执行。5.3 多用户权限与安全加固如果 paperless-ngx 部署在公司环境或者和同事共享使用一定要花时间设置用户权限。新版本已经支持每个用户组对标签、对应方、文档类型的访问控制可以在「管理界面」里分别分配权限。比如财务组只能看到发票类文档人事组只能看合同类文档admin 拥有全部权限。安全方面有几个容易被忽视的细节不要用默认的PAPERLESS_SECRET_KEY一定要改成随机长字符串如果服务器直接暴露公网务必在前面加反向代理并启用 HTTPS不要裸用 8000 端口如果你开启邮件消费功能邮件账户密码会存在配置里定期更换邮箱密码后要同步更新 paperless 的配置登录接口建议限制尝试次数新版自带了这个功能路径在「管理界面」→「用户」→「安全设置」把登录失败次数调低一点。6. 常见问题与排查技巧实录6.1 OCR 不识别或输出乱码这个问题几乎每个人都遇到过尤其是处理中文文档时。现象分两种一种是什么都识别不出来标题空着或乱码另一种是识别了但文字明显错乱。排查分三步确认语言包进入容器执行tesseract --list-langs看输出里是否包含chi_sim。如果没有说明 OCR 语言包没装上需要重新设置PAPERLESS_OCR_LANGUAGEchi_simeng后重建容器。确认输入文件质量如果原始图片分辨率低于 200 DPI、或者文字本身就模糊Tesseract 再怎么调也有限。可以用手机扫描类 App 先做一次增强再导入比系统内调参更有效。确认PAPERLESS_OCR_MODE如果你改成了skip而原 PDF 的文字层是乱码自然无法识别改成redo会重新跑一遍。我遇到过最奇葩的一种情况是某份文件识别出来的文字全是英文单词但文件内容是纯中文。后来发现是语言包优先级设置问题chi_simeng里eng排在后面但部分文档页面的中英文混排顺序让 Tesseract 误判了语言。这种情况没有完美解法多试几种语言组合或者把chi_sim放前面多数情况能改善。6.2 consumer 容器不动文档一直卡在 consume 目录导入文件后发现网页里没有新文档先看 consume 目录如果文件还在原地说明 consumer 任务没触发或者处理失败了。按这个顺序排查docker compose logs consumer查看日志这是最直接的线索如果日志里有权限报错检查 consume 目录的属主和权限容器内用户 UID 默认是 1000宿主机目录要chown -R 1000:1000 ./consume如果日志显示文件类型不支持检查文件后缀和格式paperless-ngx 对图片和 PDF 支持最好但有些加密 PDF 它处理不了如果日志完全没动静可能是 Redis 连接问题docker compose exec broker redis-cli ping看看是否返回PONG。有一种情况很坑你往 consume 目录里放了文件但文件名里带了特殊字符比如#、%、consumer 在解析文件名时可能直接跳过。我的建议是导入前统一把文件名改成简单的YYYY-MM-DD 标题.pdf格式或者干脆靠邮件消费来避免这个坑。6.3 时间显示不对和容器经常重启时间显示不对十有八九是没设置PAPERLESS_TIME_ZONE或者设置了但容器没重建。改完环境变量一定要docker compose up -d --force-recreate重建容器单纯 restart 不会让新环境变量生效。容器经常重启大概率是依赖服务没起来。paperless-ngx 的 webserver 启动时会去连数据库和 Redis如果 db 还没就绪webserver 会反复失败并触发 restart 策略。这时候别看 webserver 日志先去docker compose ps看所有容器的状态等 db 变成 healthy 再看。我在首次部署的时候遇到过这个问题解决办法是在 compose 文件里给 db 加一个健康检查和depends_on条件让它等待数据库真正就绪再启动 webserver。6.4 搜索不到已经成功导入的文档文档明明已经在列表里但搜索某个关键词却找不到。这通常是全文索引没有覆盖到新文档。解法是手动重建索引docker compose exec webserver document_index rebuild_index如果重建完还搜不到检查你的搜索关键字是不是被分词器拆掉或者被停用词过滤了。中文场景下paperless 默认的分词机制对较长的中文字段支持没问题但单字关键词搜索效果比较差。比如你搜“票”可能什么都搜不出但搜“发票”就能命中。6.5 迁移服务器时最容易被坑的路径问题我身边有人迁移 paperless-ngx 时数据卷全拷过去了但启动后网页却是空的原因是旧服务器的PAPERLESS_MEDIA_ROOT和默认路径不一致或者导出时的目录结构没完整带过来。迁移前先把环境变量列表截个图尤其是PAPERLESS_DATA_DIR、PAPERLESS_MEDIA_ROOT、PAPERLESS_CONSUMPTION_DIR这些路径变量确保新服务器上环境变量和旧服务器保持一致。还有一个容易忽略的点数据库版本。旧服务器如果跑的是 PostgreSQL 12新服务器直接拉到 PostgreSQL 15数据库文件是不能直接挂载使用的会报版本不兼容。迁移时尽量保持数据库大版本一致或者使用pg_dumppg_restore做逻辑迁移。写在最后我个人的实际使用体会是paperless-ngx 入门不难难的是坚持用下去。很多人部署完新鲜两天之后又回到把文件随便塞进网盘的旧习惯。我的经验是把“摄入”环节做到足够无脑你才能长期用它。对我来说最常用的场景就是月底报销手机上把发票拍照丢到专门的邮箱或者扔进 consume 目录剩下的事全交给它。月底打开网页搜一个“报销”标签该有的票据一张不缺光是这点节省的时间就值回部署成本了。还有一个小技巧如果你手头有大量历史扫描件不要追求一天导完可以每天丢几十份进去让 OCR 任务在夜里慢慢跑既不占用白天资源也能看着文档库一天天丰满起来。只要熬过前两个月的积累期后面你再也回不去那种在文件夹里翻半天找不到东西的日子了。
