SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何在 VPS 上用 Docker Compose 部署 Paperless-ngx

在 VPS 上部署 Paperless-ngx:使用官方 PostgreSQL Compose 栈,正确设置 PAPERLESS_URL,配置 consume 目录与 OCR 语言,并完成 HTTPS 和备份。

您要构建的内容

在 VPS 上运行 Paperless-ngx,可以将一批扫描纸质文件转换为可搜索的归档。将 PDF 放入受监控的目录后,服务器会对其运行 OCR(光学字符识别),提取文本,推测日期和通信对象,然后完成归档。安装过程只需使用一个包含 4 个服务的 Docker Compose 文件。之后的工作都属于配置,本指南的大部分内容也都集中在这里,因为安装通常会在配置阶段出错。Paperless-ngx 并不是照片库:OCR 和通信对象识别无法处理一批旅行照片 JPEG,因此应将这些文件放入专为照片设计的照片服务器,而让 Paperless-ngx 专门处理纸质文件。视频也是如此:一组翻录的电影应放在媒体服务器上。此类场景中,类似伪装成 90 年代录像租赁店的 Jellyfin 前端的界面可以将浏览作为核心功能,而不是依赖搜索。

Paperless-ngx 是原始 Paperless 项目的社区维护分支。它免费、支持自托管,并将文档以普通文件形式存储在磁盘上,因此您始终可以访问自己的文档归档,不会被锁定在系统之外。在 VPS 上运行它,而不是运行在家用设备上,可以让您从任何位置访问扫描件,无需在家用路由器上开放端口;它还适合与用于存放非纸质文件的私有 Nextcloud 实例配合使用。相同的思路也适用于连接扫描仪的桌面电脑,因为在该 VPS 上自行运行 RustDesk 中继服务器后,您可以从其他位置操作这台电脑,同样无需在路由器上打孔。

实际运行的组件

官方 compose 文件会启动 4 个容器。了解每个容器的用途后,日志更容易阅读。

  • webserver:paperless-ngx 镜像本身。它运行 Web 界面、API、监视输入目录的 consumer,以及执行 OCR 的 Celery 任务 worker。
  • db:PostgreSQL。它保存元数据、标签、联系人和全文搜索索引表,但不保存 PDF 文件。
  • broker:Valkey,一种兼容 Redis 的键值存储。它在 Web 进程与 worker 之间提供任务队列。
  • gotenberg 和 tika:可选组件,仅在 -tika compose 变体中使用。它们将 Office 文档(.docx、.xlsx、.odt)转换为 PDF,供 paperless 建立索引。

截至 October 2026,postgres compose 文件固定使用 docker.io/library/postgres:18 和 docker.io/valkey/valkey:9-alpine,并从 ghcr.io/paperless-ngx/paperless-ngx:latest 拉取应用镜像。

前置条件

  • 一台具有 sudo 访问权限的 Ubuntu 24.04 KVM VPS,并且已安装 Docker 和 Compose 插件。如果这部分对您来说是新内容,请先阅读VPS 的 Docker Compose 基础知识,然后再返回此处。
  • 一个 A 记录指向该 VPS 的域名。Paperless 不会接受通过 HTTPS 对未配置主机名的登录请求,因此这一点需要比您预期更早确认。
  • 内存是真正的限制因素。PostgreSQL、Valkey、Granian Web 服务器和 Tesseract OCR worker 同时常驻时,轻度使用 2 GB 足够。如果您计划导入数百份扫描文档的积压任务,请配置 4 GB。因为大型多页 PDF 的 OCR 处理会产生内存峰值,可能导致 worker 被内核的 out-of-memory killer 终止。
  • 磁盘:您的存档会保存两份,即原始文件和经过 OCR 处理的存档 PDF,因此磁盘空间应按扫描文件总大小的大约两倍预留。

获取官方 compose 文件

提供了一个交互式安装程序:

bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"

它会询问相关问题并替您写入文件。手动执行只需 4 条命令,而且您可以明确知道所有内容的位置。这正适合需要长期维护的服务器。

mkdir -p ~/paperless && cd ~/paperless
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
curl -fsSL -o docker-compose.env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.env
curl -fsSL -o .env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env

这些变体位于同一目录中:docker-compose.sqlite.yml、docker-compose.mariadb.yml,以及各自的 -tika 版本。新安装请选择 postgres。SQLite 可满足几百份文档的需求,但全文搜索索引的速度会在 PostgreSQL 之前明显下降。

.env 文件包含一行内容:COMPOSE_PROJECT_NAME=paperless。该名称会成为每个容器和卷名称的前缀。因此不要删除它,然后再疑惑为什么 docker compose down -v 找不到您的数据。

首次启动前配置 docker-compose.env

有两个设置不是可选项。使用项目文档提供的命令生成密钥:

python3 -c "import secrets; print(secrets.token_urlsafe(64))"

然后编辑 docker-compose.env:

PAPERLESS_SECRET_KEY=<the long string you just generated>
PAPERLESS_URL=https://paperless.example.com
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu+eng
USERMAP_UID=1000
USERMAP_GID=1000

PAPERLESS_SECRET_KEY 的初始值是字面量 change-me。它用于签名会话令牌。当前版本会将该占位值视为未设置:密钥缺失、为空或仍为 change-me 时,paperless 会拒绝启动。因此,替换该值之前,整个服务栈都无法启动。请在首次启动前设置它,因为之后更改会使所有用户退出登录。

PAPERLESS_URL 是最容易节省排查时间的设置。Paperless 是一个 Django 应用,Django 会检查 Host 请求头,以及登录表单中浏览器报告的来源。设置 PAPERLESS_URL 后,系统会自动将你的地址加入 ALLOWED_HOSTS、CORS_ALLOWED_HOSTS 和 CSRF_TRUSTED_ORIGINS。如果留空并将域名指向此主机,登录页面仍会加载,因为 PAPERLESS_ALLOWED_HOSTS 默认为 *;但通过 HTTPS 提交登录表单时会因 CSRF verification failed. Request aborted. 失败,因为你的域名不是受信任的来源。填写时不要添加末尾斜杠或路径。

USERMAP_UID 和 USERMAP_GID 用于设置容器运行时使用的用户。将它们设置为你自己的账户,并使用 id -u 和 id -g 确认账户信息。如果两者不匹配,你复制到 consume 文件夹中的文件将无法被 consumer 读取,日志中会显示权限错误,而不是导入成功。

启动堆栈并创建第一个用户

docker compose pull
docker compose up -d
docker compose run --rm webserver createsuperuser
docker compose logs -f webserver

createsuperuser 会提示您输入用户名、电子邮件地址和密码。系统没有默认登录凭据。如果跳过此步骤,首次打开 Web 界面的用户将被要求创建超级用户;而在 8000 端口仍对互联网开放时,该用户可能不是您。因此,请立即从 shell 创建账户。等待日志显示服务器正在监听 8000 端口后,再尝试使用浏览器访问。首次启动还会执行数据库迁移,这通常需要 1 到 2 分钟。

在配置域名之前,先在本地检查:

curl -I http://127.0.0.1:8000

302 重定向到 /accounts/login/,表示堆栈运行正常。

在前端配置 HTTPS

默认的 compose 文件会发布 8000:8000,并绑定到所有网络接口。在公共 VPS 上,这会让任何找到该地址的人都能通过明文 HTTP 访问整个文档归档。将端口行修改为仅绑定到 loopback:

    ports:
      - "127.0.0.1:8000:8000"

然后在反向代理中终止 TLS(传输层安全),并将请求转发到 127.0.0.1:8000。如果这台服务器上只有这一个应用,使用任何带有 ACME(自动证书管理环境)客户端的代理即可。如果要在同一套证书配置后运行多个容器,请参照多个 Docker Compose 应用使用 Traefik 反向代理的配置模式,将 webserver 服务加入代理网络,并完全取消发布端口。

无论使用哪种代理,登录表单的来源检查都是 HTTPS 配置最容易出问题的地方。除非设置 PAPERLESS_PROXY_SSL_HEADER,否则 Paperless 会忽略 X-Forwarded-Proto;该选项默认关闭。因此,Django 会将所有经过代理的请求视为明文 HTTP,而浏览器发送的 https:// 来源只有在列入受信任来源后才会通过检查。将 PAPERLESS_URL 设置为你在浏览器中输入的确切 https:// 地址,就会将其加入该列表。未设置时,页面看起来正常,但会出现 CSRF verification failed. Request aborted.。

还应提高代理的上传大小限制。通过将请求体限制为 1 MB 的代理上传 40 MB 的扫描文件时,请求会在 paperless 收到之前被拒绝,浏览器只会报告通用的上传失败。

consume 目录的工作方式

Compose 文件会将 Compose 目录中的 ./consume 绑定挂载到容器中。放入该目录的文件会被导入,随后从该目录删除,因为文件现在已由 paperless 管理,并存储在 media 卷中。

cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserver

您应能看到 consumer 获取文件名、执行 OCR,最后输出一行报告,说明文档已添加。对于一页扫描件,整个过程只需几秒;对于较长的文档,可能需要 1 分钟或更久。

有两个设置会改变文件的查找方式。PAPERLESS_CONSUMER_RECURSIVE=true 会让 paperless 在子目录中查找,PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true 会将每个子目录名称转换为标签。因此,将文件放入 consume/invoices/2026/ 后,文件会被标记为 invoices 和 2026。这是您能构建的成本最低的归档系统。

检测是另一部分。默认情况下,PAPERLESS_CONSUMER_POLLING_INTERVAL 为 0,这表示 paperless 使用原生文件系统通知,并且通知会立即触发。在网络文件系统上,这些通知并不可靠。如果您的 consume 目录是 NFS 或 SMB 共享目录,网络扫描仪可以将文件写入其中,那么文件可能会一直未被检测到。解决方法是将间隔设置为正数秒,使 paperless 改为扫描该目录。

OCR 语言及其成本

PAPERLESS_OCR_LANGUAGE 接受 3 个字母的 Tesseract 语言代码,默认值为 eng。使用加号组合多个语言,例如 deu+eng。随后,Tesseract 会在每一页中使用列出的所有语言,因此每增加一种语言,每次扫描都会增加 CPU 时间。对于共享 vCPU 的 VPS,这可能决定扫描是在 10 秒内完成,还是需要 1 分钟。只列出文档实际使用的语言。

该镜像内置英语、德语、意大利语、西班牙语和法语。要支持其他语言,请将语言以空格分隔的列表形式添加到 PAPERLESS_OCR_LANGUAGES,例如 PAPERLESS_OCR_LANGUAGES=tur ces,然后重启。容器会在启动时下载 Tesseract 数据包,因此修改后的首次启动会更慢。

备份数据库和媒体文件

PostgreSQL 运行时复制 Docker 卷,得到的备份可能无法恢复。Paperless 提供了自己的导出工具,它会将文档以及包含全部元数据的 JSON 清单写入 ./export 绑定挂载:

docker compose exec -T webserver document_exporter ../export --delete --no-progress-bar

--delete 会删除不再对应当前文档的导出文件,使该目录保持镜像状态,而不是无限增长。--no-progress-bar 可让 cron 执行时保持输出简洁,-T 则可阻止 Compose 请求 cron 不具备的终端。

在新的堆栈上,恢复操作可针对同一目录执行 document_importer。因此,只需妥善保管导出目录即可。按计划将其发送到异地,并使用 来自 VPS 的加密、去重 restic 备份;先执行导出,再运行 restic,这样 restic 永远不会捕获写入未完成的归档。

通过确认 export/manifest.json 存在,并检查文件数量是否与界面中的文档数量一致,验证备份是否有效。从未列出内容的备份不能算作备份。每晚导出任务如果悄悄失败,情况会更糟。因此,让 cron 任务将退出状态推送到 您自己的 ntfy 服务器,这样您会在任务失败的那一周得知问题,而不是等到需要恢复的当天才发现。

FAQ

为什么将域名指向该服务后每个页面都返回“Bad Request (400)”?

使用默认文件时不应出现此问题,因为 PAPERLESS_ALLOWED_HOSTS 默认设置为 *。如果容器日志中出现带有 DisallowedHost 的 400,说明 PAPERLESS_ALLOWED_HOSTS 被设置为一个不包含您的域名的列表。在 docker-compose.env 中设置 PAPERLESS_URL=https://paperless.example.com,且末尾不要加斜杠。paperless 会将该主机名加入列表。也可以删除 PAPERLESS_ALLOWED_HOSTS 行。然后运行 docker compose up -d 重新创建容器。仅编辑环境变量文件不会生效,因为运行中的容器会继续使用启动时加载的环境变量。

我将 PDF 放入 consume 文件夹后没有任何反应。哪里出错了?

先检查 docker compose logs webserver。权限错误表示 USERMAP_UID 和 USERMAP_GID 与文件所有者账户不匹配,因此请修正它们并重新创建容器。完全没有日志记录表示文件事件根本没有到达。这种情况在网络共享上较常见,因为原生文件系统通知在此类环境中并不可靠。将 PAPERLESS_CONSUMER_POLLING_INTERVAL 设置为类似 30 的值,paperless 就会改为每 30 秒扫描一次该文件夹。

我可以使用 SQLite 而不是 PostgreSQL 运行 paperless-ngx 吗?

可以。docker-compose.sqlite.yml 受支持且占用的内存更少,适合小型 VPS。随着归档增长,取舍会变得明显:当文档数量达到数千份时,全文搜索和批量编辑标签会明显变慢。以后迁移需要先导出再导入。因此,如果预计归档会持续增长,现在就应选择 PostgreSQL。

扫描归档实际需要多少磁盘空间?

大约是源文件大小的两倍。Paperless 会保留未修改的原始文件,并存储一份带可搜索文本层的 OCR PDF,以及较小的缩略图。仅包含文本的 200 KB 扫描文件占用空间很小。长篇合同的 30 MB 彩色扫描件大约会占用 60 MB。若将导出目录保存在同一磁盘上,还要计入该目录;这样同一份归档在磁盘上的占用量会达到原始大小的 3 倍。

我需要 Tika 和 Gotenberg 容器吗?

只有在您希望将 Word、Excel 或 OpenDocument 文件与 PDF 一起建立索引时才需要。它们会将这些格式转换为 PDF,供 paperless 进行 OCR 和搜索。它们还会增加 2 个运行中的容器,并占用几百 MB 内存。因此,如果您归档的内容已经全部是 PDF 或图像文件,在小型服务器上可以跳过它们。

#paperless-ngx#documents#自托管#Docker#ocr