Paperless-ngx 部署教程:中文 OCR、文档检索与备份
使用 VPS 部署 Paperless-ngx,介绍配置、访问方式、实际操作及数据备份。
开始前
合同已经扫描成 PDF,发票也下载到了电脑里,过几个月却想不起文件名。需要找某个订单号时,只能挨个打开。这类文件放进普通网盘后,目录是整齐了,纸面上的文字未必能直接搜索。
Paperless-ngx 可以接收 PDF 和图片,识别扫描件里的文字,再按标签、往来单位、文档类型和日期整理。你可以在浏览器里查找正文、查看原件和修改分类。它适合有持续归档需求的个人或小团队:收集报销凭证、合同、说明书、账单,把它们整理成一个能检索的资料库。
下面固定 Paperless-ngx 3.3.1,先保留本机访问入口,完成管理员初始化,再按自己的网络条件接入 HTTPS。文档入库后,分别检查中文检索、普通账号权限和同版导出恢复;首页能打开还不够。全文搜索与 OCR 都能在自己的服务器上完成,无需为了这个用途接入大模型。
先确认这些文件适不适合放上 VPS
Paperless-ngx 的重点是文档归档。它保留原始文件,提取文字,并根据文件和设置生成可供浏览的归档版本。标签与文档类型让一张发票既能按月份查,也能按供应商查,不必把同一个文件复制到几个目录里。
它不会替你确认合同条款,也不能保证 OCR 读出的金额、身份证号或账户号码准确。搜索帮助找到文件,最后使用其中的信息时仍要对照原件。照片歪斜、印章覆盖文字、低分辨率和手写内容都可能影响识别。
选择部署位置之前,要看这些资料是否适合保存到那台机器。普通 OCR 使用本地 Tesseract;但自托管不等于文件已经加密。Paperless-ngx 的功能说明明确提到,文件以明文形式存放在磁盘上。拿到服务器磁盘、备份或足够权限的管理员,可能读到这些材料。HTTPS 保护传输,无法代替服务器存储和账户权限管理。
个人说明书、公开资料和普通票据可以先作为入门样本。包含商业秘密、客户身份资料或其他敏感内容时,先确认保存位置、授权和备份方式是否合适;不适合上公网的资料,可以放在内网服务器,通过自己的安全访问通道使用。不要为了“能在手机打开”而直接公开整套文档库。
项目还提供可选 AI 和远程 OCR 功能,但这些有各自的配置与数据流向。本文只使用本地 OCR,不开启它们。
给 OCR 留资源,也给原件留空间
一个只有几位使用者、按天少量导入文件的文档库,可以从 2 核、4GB 内存的 VPS 考虑起。这是本文对小规模用途的建议,不是官方最低配置,也不是吞吐量承诺。批量识别几百页 PDF、同时运行其他应用时,要另外估算内存和处理时间。
OCR 往往在导入阶段集中使用 CPU 和内存,平时浏览文档则是另一种负载。选机器时不要只看“首页能打开”,还要考虑最常导入的文件页数、清晰度和图片尺寸。本文把后台任务和每个任务的线程数都从 1 开始,避免新手第一次导入就同时压上多个识别任务;确认资源余量后再调。
存储要容纳原件、归档文件、缩略图、数据库和导出副本。已有一批扫描件时,先统计其总容量,再给后续资料和临时导出留余量。磁盘快满时,继续导入和备份都可能失败。
选择现有 VPS 或购买新实例时,确认完整 Docker 支持、磁盘可扩展方式、流量及备份安排即可。例如雨云服务器可以作为选型入口,实际按当前机房、规格和价格比较。这里的任务会用到 OCR 算力和持久化存储,不要只因套餐便宜就选一个内存不足、还承载很多其他应用的实例。
准备 Linux 管理账号、可用的 Docker Engine 与 Compose 插件。没有安装时按 Docker 官方安装文档操作;有面板或已有 Docker 的服务器,先确认版本和现有端口,不要重复安装一套。
docker --version
docker compose version
本文使用的管理账号需要有权限调用 Docker。该权限可以控制宿主机,应只给可信的管理员。Caddy 使用宿主机服务形式;如果你已有容器形式的反向代理,需要改为容器网络互通,不能直接照搬后面的回环地址。
先建立目录和两份私有配置
这里选择 PostgreSQL 数据库与 Valkey 消息代理。Valkey 是兼容 Redis 协议的服务,所以 Paperless 的连接设置仍叫 PAPERLESS_REDIS。3.3.1 的官方 Compose 也采用这类组合。本文固定 PostgreSQL 18.6 和 Valkey 9.1.2 的镜像内容,不让同一个标签悄悄换成另一批字节。
先在普通管理账号的家目录建立项目:
umask 077
mkdir -p ~/paperless/consume ~/paperless/export
cd ~/paperless
app_db_password="$(openssl rand -hex 32)" || exit 1
printf 'PAPERLESS_DB_PASSWORD=%s\n' "$app_db_password" > .env
printf 'POSTGRES_BOOTSTRAP_PASSWORD=%s\n' "$(openssl rand -hex 32)" >> .env
printf "CREATE ROLE paperless LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD '%s';\nCREATE DATABASE paperless OWNER paperless;\n" "$app_db_password" > init-app-role.sql
unset app_db_password
printf 'PAPERLESS_SECRET_KEY=%s\n' "$(openssl rand -hex 64)" > docker-compose.env
printf 'USERMAP_UID=%s\nUSERMAP_GID=%s\n' "$(id -u)" "$(id -g)" >> docker-compose.env
chmod 600 .env docker-compose.env init-app-role.sql
这几行生成数据库初始化账号与应用账号的不同密码、应用签名密钥,并记录宿主机管理账号的 UID 和 GID。init-app-role.sql 只供新数据库初始化时执行,创建不具备超级用户、建库、建角色、复制或绕过行级安全权限的应用账号。它们应在首次安装时执行,后续更新不要再次覆盖这些私有文件。更换数据库环境变量不会自动修改已初始化数据库里的密码;随手重生成密钥也可能影响已有会话。
在 docker-compose.env 后面补上以下设置,把域名换成自己的地址:
PAPERLESS_URL=https://docs.example.com
PAPERLESS_TIME_ZONE=Asia/Shanghai
PAPERLESS_OCR_LANGUAGE=chi_sim+eng
PAPERLESS_OCR_MODE=auto
PAPERLESS_TASK_WORKERS=1
PAPERLESS_THREADS_PER_WORKER=1
PAPERLESS_OCR_LANGUAGE=chi_sim+eng 选择实际用于识别的简体中文和英文模型。多种语言会增加处理开销。本文给中文模型单独固定来源和哈希,挂入容器后先用 tesseract --list-langs 确认加载。
在有 dpkg-deb 的 Debian 或 Ubuntu 管理环境中,从官方 Debian 包提取语言数据:
cd ~/paperless
command -v dpkg-deb >/dev/null || { echo '需要 dpkg-deb,先准备受支持的提取工具'; exit 1; }
curl --fail --location --output tesseract-ocr-chi-sim_4.1.0-2_all.deb https://deb.debian.org/debian/pool/main/t/tesseract-lang/tesseract-ocr-chi-sim_4.1.0-2_all.deb
printf '%s %s\n' 035f20a3e317343c1b88f6db546225eda9b5857078729c009662e0b3e0fb5d57 tesseract-ocr-chi-sim_4.1.0-2_all.deb | sha256sum --check - || exit 1
dpkg-deb --extract tesseract-ocr-chi-sim_4.1.0-2_all.deb language-package
install -m 644 language-package/usr/share/tesseract-ocr/5/tessdata/chi_sim.traineddata ./chi_sim.traineddata
printf '%s %s\n' a5fcb6f0db1e1d6d8522f39db4e848f05984669172e584e8d76b6b3141e1f730 chi_sim.traineddata | sha256sum --check - || exit 1
本文的隔离实验在 ARM64 上使用这个文件,容器里的 Tesseract 是 5.5.0。语言数据以只读文件挂载,先于应用启动准备好。文件自身不限定 CPU 架构,但这不等于已测过 AMD64 整套部署。
另一条官方支持的路径是设置 PAPERLESS_OCR_LANGUAGES=chi-sim,让默认启动脚本安装额外语言包。这里的 chi-sim 是包名,识别代码仍是 chi_sim。启动安装需要可用的软件包网络,也不兼容强制无 root 启动;本次没有实测该安装路径。本文不额外设置 user: 1000:1000,仍使用镜像默认启动与用户映射方式,不能据此称为已验证的 rootless 部署。
PAPERLESS_URL 要包含协议,不带结尾斜杠,也不要写成 https://example.com/paperless。它会帮助配置允许的主机与请求来源,适合独立子域名。详细含义见 Paperless-ngx 配置文档。
写 Compose,数据库端口不要向公网开放
创建 docker-compose.yml:
services:
broker:
image: docker.io/valkey/valkey@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres@sha256:74935e72241653ca55e0414067e6d8763aceb8a810eb51b452253ec3dcfc4336
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql
environment:
POSTGRES_DB: bootstrap
POSTGRES_USER: bootstrap
POSTGRES_PASSWORD: ${POSTGRES_BOOTSTRAP_PASSWORD:?set POSTGRES_BOOTSTRAP_PASSWORD}
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:3.3.1@sha256:cb91bfb468e63025a9d10c01b54c33665113668eee61b490db96de19b860b9c4
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "127.0.0.1:8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
- ./chi_sim.traineddata:/usr/share/tesseract-ocr/5/tessdata/chi_sim.traineddata:ro
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBNAME: paperless
PAPERLESS_DBUSER: paperless
PAPERLESS_DBPASS: ${PAPERLESS_DB_PASSWORD:?set PAPERLESS_DB_PASSWORD}
volumes:
data:
media:
pgdata:
redisdata:
PostgreSQL 官方镜像的 POSTGRES_USER 会创建有高权限的初始化账号,不能把它称为普通业务账号。这里的 bootstrap 只用于初始化;应用连接的是随后创建的普通 paperless 账号。应用密码和初始化 SQL 从同一个生成值取得,避免两边不一致。db 和 broker 只在项目网络里提供服务,没有给宿主机发布 5432 或 6379 端口。
这里用的是 PostgreSQL 18,数据卷挂到 /var/lib/postgresql。这是该版本官方 Compose 文件采用的路径,不要把旧教程中的 /var/lib/postgresql/data 不加核对地搬过来。已经有 PostgreSQL 旧版本数据卷时,也不能只把镜像改成 18;数据库大版本迁移需要单独安排。
data 保存应用数据,media 保存文档等媒体,pgdata 保存数据库。consume 是投递文件的入口,export 是导出目录。consume 用于接收待处理文件,不能当成归档原件目录。
Web 端口只绑定 127.0.0.1。所以启动后,即使在云防火墙开放 8000,外部也不能直接访问这个端口。先通过本机入口初始化,随后由反向代理提供正式访问。
检查配置并拉取镜像:
docker compose config --quiet || exit 1
docker compose pull || exit 1
docker compose up -d db broker || exit 1
# 等数据库完成初始化;失败时先看 db 日志,不继续创建账号。
for attempt in $(seq 1 30); do
docker compose exec -T db pg_isready -U bootstrap -d bootstrap && break
sleep 2
done
docker compose exec -T db pg_isready -U bootstrap -d bootstrap || exit 1
if ! docker compose exec -T db psql -v ON_ERROR_STOP=1 -U bootstrap -d bootstrap < init-app-role.sql; then
echo '应用数据库初始化失败,停止后续启动'; exit 1
fi
docker compose up -d webserver || exit 1
docker compose ps
docker compose logs --tail=100 webserver
docker compose exec -T webserver tesseract --list-langs
config --quiet 会检查 Compose 配置,避免把包含展开密码的完整配置输出到终端记录里。三个镜像摘要是多平台索引,客户端再按机器架构选择子镜像;本次实际运行的是 ARM64 子镜像。拉取镜像后,首次启动还会准备数据库和运行迁移,需要根据日志判断完成状态;只有容器列表出现名字,不代表应用已经能处理文档。
上面的账号初始化命令只用于全新的 PostgreSQL 数据卷,已有账号和数据时不要重复执行。查询 pg_roles 可以检查 paperless 的五项高权限标志都为 false;数据库所有者权限仍允许它创建自己的表,不代表它能管理整个数据库服务器。
首次启动卡住时,保留日志里的错误。镜像拉取、容器启动后下载中文语言包、数据库连接是不同步骤,不应一律归为“Docker 出问题”。
初始化管理员,再接入正式域名
在自己的电脑上建立 SSH 转发,将示例账号和服务器地址换成实际值:
ssh -L 18000:127.0.0.1:8000 your-user@your-server-ip
保持连接,浏览器访问 http://127.0.0.1:18000。Paperless-ngx 3.3.1 的首次访问会提示创建超级用户,按页面完成初始化。因为配置里提前设置了正式域名,回环地址访问如果被主机或来源检查拦截,可以临时在 docker-compose.env 增加:
PAPERLESS_ALLOWED_HOSTS=127.0.0.1,localhost
PAPERLESS_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:18000
修改环境文件后,需要重新创建应用容器才会读到新值:
docker compose up -d webserver
这些设置与 PAPERLESS_URL 产生的值合并,因此不需要把正式域名改成回环地址。完成初始化并确认正式域名可访问后,移除临时加入的本地访问设置,再重新创建应用容器。这样首次设置账户的页面不会先暴露在公网。
准备正式域名,添加指向 VPS 的 A 记录,并确认已有 AAAA 记录有效。国内地域的公开网站要满足相应备案与接入要求。宿主机已有 Caddy 时,在现有配置中增加站点块:
docs.example.com {
reverse_proxy 127.0.0.1:8000
}
按照自己现有的 Caddy 管理方式校验并重载配置。使用常规 systemd 安装、配置路径为 /etc/caddy/Caddyfile 时:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
证书签发需要域名与网络满足验证条件。防火墙开放正式 Web 入口,保留 SSH 管理入口,不必公开应用、数据库和消息代理端口。此例的 Caddy 在宿主机上运行;容器里的 127.0.0.1 指向容器自己,采用容器反向代理时要改连接方式。
这里的 DNS、宿主机防火墙与 Caddy 自动证书配置是按官方机制给出的部署步骤,本次本机隔离实验没有验证公网域名、ACME 或 Linux 裸机链路。打开 https://docs.example.com,用刚创建的账号登录。若页面能显示但提交时报 CSRF 错误,先检查域名与 PAPERLESS_URL 是否一致、环境文件是否已被重新读取,不要直接关闭 CSRF 保护。
导入第一批文件时,先验证中文识别
先挑少量有代表性的文件:一份带原生文字的 PDF、一份中文扫描 PDF、一张手机拍摄的清晰图片。上传后等任务完成,打开详情页,查看预览、提取出的文字和识别出的日期。再选一个原件里明确存在的词或编号搜索,确认它能指向正确文档。中文识别成功与搜索结果正确是两项检查,应分别核对,不只看任务状态。
本次用合成图片完成了入库和检索,数字 31415926、27182818 与“中文识别”能够检索到对应文档;“仅用于测试”一行却被识别成了其他字符。它证明这套固定版本与语言文件能处理该样本,不能证明文字、金额或任意扫描件都会准确,更不是性能测量。
auto 模式会判断 PDF 是否已有足够的文字层,存在时可能跳过 OCR。这适合原生 PDF 与扫描件混合的资料库。如果扫描软件已有错误文字层,不能只因“搜索有结果”就认定识别无误;要对照正文内容。redo 和 force 有各自代价,尤其 force 会把页面栅格化,可能让文件变大、文字放大后不如原件清晰,不宜作为全部文件的默认处理。
文档详情把文件预览和分类信息放在一起。核对编号或金额时,应查看原件,不只依靠搜索结果摘要。
简体中文文件识别为空或日志提示缺语言数据时,回到语言包安装和语言代码设置排查。照片中的字很小、背景复杂或有明显倾斜,则要先改善扫描质量。OCR 参数无法补回原件里已经缺失的信息。
标签先从用途明确的少量分类开始,例如“待核对”“已报销”;文档类型用“发票”“合同”“说明书”;往来单位记录开票方或合同对方。不要把每个日期和文件名都做成标签。归档以后还能调整分类,没有必要第一天就建几十个空标签。
投递目录会清空文件,权限也要另设
文件可以在网页上传,也可以放到宿主机的 consume 目录。目录中的文件被成功接收后,会从该目录移走并存入 Paperless 管理的文档存储。因此,向这个目录投递副本;不要把电脑里唯一一份材料所在的目录直接挂载成消费目录。
初期先使用网页上传,熟悉处理情况后再连接扫描仪或同步任务。若投递目录位于 NFS 等不支持文件变动通知的文件系统上,可能需要设置轮询;具体见安装文档的文件系统说明。不要把文件迟迟未导入直接理解为 OCR 慢。
多用户使用时,每个人建立自己的账号。超级用户能看全部文档,适合维护,不适合用来验证普通成员的隔离效果。日常使用可以建立普通账号,根据需要给文档查看、上传或编辑权限。
尤其注意消费目录导入:官方说明,这种文档默认没有所有者和额外对象权限;无所有者对象会对有相应应用访问权限的用户可见。团队使用前,应通过工作流安排所有者或文档权限,并用两个普通账号分别检查可见范围。给标签设置权限,也不会自动限制带该标签的文档。
隔离实验给两个普通账号相同的全局文档查看权限,只给其中一个 doc1 的对象查看权限。它能列出并读取该文档,修改请求返回 403;另一个账号的列表为空,读取也被拒绝。导入新环境后同样检查了这组允许与拒绝结果。超级用户能看见文件,不能替代这项验证。
文档有自己的所有者、查看与编辑权限。它与用户能否进入应用、能否操作标签等全局权限是两层设置,详见用户与权限说明。
分享链接会提供另一条访问路径。发送之前确认文件内容和有效期,不要因为日常登录有密码就默认所有分享都受相同限制。
导出文档之后,还要保存部署配置
从项目目录执行:
cd ~/paperless
docker compose exec -T webserver document_exporter ../export
导出器会写出文档、缩略图和记录元数据的 manifest.json 等内容,容器内的 ../export 对应宿主机项目下的 export。成功导出后,把整个导出结果复制到独立存储,另外私密保存 Compose 文件、环境文件、init-app-role.sql、固定语言文件及恢复所需的镜像版本。导出不是磁盘快照,API 令牌不在这份恢复承诺内;新环境要重新安排令牌,不能从浏览器还可登录推断它们已恢复。
仅在这台 VPS 上留一个导出目录,无法应对实例丢失。导出本身也包含敏感文档和元数据,不能放进公开网页目录。默认导出不会清理以前导出中已经删除的旧文件;需要清理时先了解删除选项的范围,避免指向混有其他资料的目录。
正式资料库安排导出时,可以暂停导入、编辑和删除,让这一批备份有清楚的时间范围。数据库备份与媒体卷备份要配套保存,不能只复制数据库,就认为原件也跟着保存了。
恢复文档导出时,把完整结果放进新环境的 export 目录,在版本匹配、数据库和文档目录为空的安装中使用:
cd ~/paperless
# 只对新建、未收录文件且没有自建应用用户的恢复环境执行。
docker compose exec -T webserver python manage.py shell -c "from documents.models import Document; from django.contrib.auth.models import User; assert Document.global_objects.count() == 0; assert User.objects.exclude(username__in=['consumer', 'AnonymousUser']).count() == 0" || exit 1
if ! docker compose exec -T webserver document_importer ../export; then
echo '导入失败,保留日志;不要开放访问或重复导入'; exit 1
fi
docker compose exec -T webserver document_sanity_checker || exit 1
在恢复服务器先安全复制完整导出目录和前面列出的配置、语言文件,核对文件完整性和目录权限;还要确认配置中的 UID/GID 与新机器的目录所有权相配。按同一版本配置启动新的数据库、消息代理与应用,确认文档库为空,再导入;此时保持回环访问,尚不接入正式域名。初始化数据库的普通账号需要相同的连接配置,但不要在导入前另外创建会冲突的应用用户。
这是新环境恢复命令,不要对正在使用的资料库直接执行。官方建议导出和导入使用相同版本;导入后检查文档数量、原件下载、中文检索和普通账号权限。确认恢复路径之后,再把新环境投入使用。
本次实际恢复使用另一个空数据库、独立数据卷和同版 3.3.1 应用。恢复后的原始 PNG、归档 PDF、WEBP 缩略图和文件元数据响应分别与源环境逐字节比较,标题、识别正文、所有者、对象权限和版本校验和也一致;恢复后又录入一份新的合成图片并完成识别和检索。
这些结果不等于所有字段都逐字节不变。导出中的部分时间精度变成毫秒,恢复后的文档 added 和版本 added 因而与源 API 的微秒值不同;文档 modified 也变成此次导入时的时间。除这三个明确核对的路径外,本次 doc1 的其他 API 字段一致。若你的业务依赖这些时间戳,应先用自己的样本核对,不把导入当成完整审计时间线的复刻。参数和注意事项见导出与导入文档。
更新时把应用与数据库分开考虑
本例固定应用镜像为 3.3.1,于 2026 年 10 月 12 日发布。本文验证的是新建隔离环境和同版导出导入,不是把旧资料库升级到该版本。官方 v3 迁移说明要求 v2 环境先到 2.20.15,不能跳过自己的起点和升级前提。
有新应用版本时,先看升级要求,完成站外备份,再修改应用镜像标签,执行拉取与启动。升级可能运行数据库迁移,出问题时不能保证只把镜像换回旧标签就能恢复。数据库和文档要有同一个恢复计划。
PostgreSQL 的大版本升级是另一项工作,本例固定的 PostgreSQL 18.6 不应随手改成未来的大版本并复用原数据卷。同理,更新 .env 中的密码只改变传入容器的值,不会改动已有数据库账户。需要变更密码时,应同步修改数据库账户与应用连接设置。
平时观察导入队列、日志、磁盘空间和失败任务。OCR 期间网页变慢,可以先减少同时处理的文件数量,检查资源占用,再决定是否增加任务并发或升配。接入批量扫描和邮件收集前,先用少量文件核对导入、检索、权限与恢复。
推荐码:KuZhuJi




