VPSにPaperless-ngxを構築する方法
Docker Composeで公式Postgres構成を使い、PAPERLESS_URL、consumeフォルダー、OCR言語、HTTPS、バックアップまで設定する手順を解説します。
構築するもの
VPS 上の Paperless-ngx を使うと、スキャンした書類を保存したフォルダーが検索可能なアーカイブになります。監視対象のディレクトリに PDF を置くと、サーバーが OCR(光学文字認識)を実行し、テキストを抽出して、日付と対応者を推測したうえで整理します。インストールは 4 つのサービスを定義した 1 つの Docker Compose ファイルで完了します。その後は設定が中心です。インストールで問題が発生するのは主にこの設定部分であるため、このガイドでも大半を設定の説明に割いています。Paperless-ngx は写真ライブラリではありません。OCR と対応者の推測は、旅行で撮影した JPEG のフォルダーには役立たないため、それらは 写真向けに構築されたフォトサーバー に保存し、Paperless-ngx は書類に使用してください。
Paperless-ngx は、元の Paperless プロジェクトを継続的に保守しているコミュニティフォークです。無料で利用でき、自己ホストできます。ドキュメントはディスク上に通常のファイルとして保存されるため、自分のアーカイブから締め出されることはありません。自宅のマシンではなく VPS で実行すれば、自宅ルーターのポートを開放せずに、どこからでもスキャンデータへアクセスできます。また、紙ではないファイル用のプライベートな Nextcloud インスタンスとも相性がよくなります。同じ考え方は、スキャナーを接続しているデスクトップにも当てはまります。その VPS 上に自分で用意した RustDesk リレーを使えば、ルーターに穴を開けずに、別の場所からそのマシンを操作できます。
実際に動作するスタック
公式の compose ファイルは 4 個のコンテナを起動します。それぞれの役割を把握すると、ログを読みやすくなります。
webserver: paperless-ngx 自体のイメージです。Web インターフェース、API、入力フォルダーを監視する consumer、OCR を実行する Celery タスクワーカーを動かします。db: PostgreSQL です。メタデータ、タグ、対応者、全文検索用のインデックステーブルを保持します。PDF は保持しません。broker: Redis 互換のキーバリューストアである Valkey です。Web プロセスとワーカーの間でタスクキューとして機能します。gotenbergとtika: オプションです。-tikaの compose バリアントでのみ使用します。Office ドキュメント(.docx、.xlsx、.odt)を PDF に変換し、paperless でインデックスを作成できるようにします。
2026 年 7 月時点では、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 と、Compose plugin がインストール済みの Docker。ここが初めての場合は、まず VPS 向け Docker Compose の基礎 を確認してから戻ってきてください。
- VPS を指す A record が設定されたドメイン名。Paperless は、指定されていないホスト名では配信しません。そのため、想定より早い段階でこの設定が必要です。
- 実際の制約はメモリです。PostgreSQL、Valkey、gunicorn、Tesseract OCR worker を同時に常駐させても、軽い用途なら 2 GB に収まります。数百件のスキャンをまとめて取り込む場合は 4 GB を割り当ててください。大きな複数ページ PDF の OCR によってメモリ使用量が急増し、kernel の out-of-memory killer が worker を終了させることがあるためです。
- ディスク: アーカイブは元のファイルと OCR 済みのアーカイブ PDF の 2 つとして保存されます。そのため、スキャンデータのサイズの約 2 倍を目安に容量を確保してください。
公式の 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 という1行が含まれています。この名前がすべてのコンテナとボリュームのプレフィックスになるため、このファイルを削除してから docker compose down -v がデータを見つけられない理由を調べることにならないようにしてください。
初回起動前に docker-compose.env を設定する
2 つの設定は必須です。プロジェクトのドキュメントに記載されたコマンドで Secret key を生成します。
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=1000PAPERLESS_SECRET_KEY は、初期状態ではリテラル値 change-me です。これはセッション Cookie の署名に使われるため、初期値のままにすると、値を知っている誰でもセッションを偽造できます。後から変更するとすべてのユーザーがログアウトされるため、初回起動前に設定してください。
PAPERLESS_URL は、1 時間の調査を省ける設定です。Paperless は Django アプリケーションであり、Django はすべてのリクエストの Host ヘッダーを検証します。PAPERLESS_URL を設定すると、ALLOWED_HOSTS、CORS_ALLOWED_HOSTS、CSRF_TRUSTED_ORIGINS が自動的に設定されます。空のままにしてドメインをサーバーへ向けると、すべてのページが Bad Request (400) を返し、コンテナのログに DisallowedHost が出力されます。末尾にスラッシュやパスを付けずに記述してください。
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 webservercreatesuperuser では、ユーザー名、メールアドレス、パスワードの入力を求められます。デフォルトのログイン情報はないため、この手順を省略すると、何を入力しても受け付けられないサインインページが表示されます。ブラウザーで接続する前に、サーバーがポート 8000 で待ち受けていることを示すログ行が出力されるまで待ちます。初回起動時にはデータベースのマイグレーションも実行されるため、1〜2 分かかります。
ドメインを設定する前に、ローカルで確認します。
curl -I http://127.0.0.1:8000302 から /accounts/login/ へのリダイレクトが表示されれば、スタックは正常に動作しています。
前段に HTTPS を配置する
標準の compose ファイルは 8000:8000 を公開しており、すべてのインターフェースにバインドされます。公開 VPS では、アドレスを知っているすべてのユーザーに対して、ドキュメントアーカイブ全体を平文の HTTP で提供することになります。ポートの行を変更し、loopback のみにバインドします。
ports:
- "127.0.0.1:8000:8000"次に、リバースプロキシで TLS(transport layer security)を終端し、127.0.0.1:8000 に転送します。このサーバーで実行するアプリがこれだけであれば、ACME(automatic certificate management environment)クライアントを備えた任意のプロキシを使用できます。1 つの証明書設定の背後で複数のコンテナを実行する場合は、複数の Docker Compose アプリケーション向け Traefik リバースプロキシ構成に従い、公開ポートを設定せずに webserver サービスをプロキシネットワークに接続します。
使用するプロキシにかかわらず、X-Forwarded-Proto: https を送信する必要があります。これがないと、Django はリクエストが HTTP 経由で到着したと判断します。そのため、ログインフォームのオリジンチェックに失敗し、正しく表示されているように見えるページで CSRF verification failed. Request aborted. が発生します。この修正のもう一方の要件は、PAPERLESS_URL に、ブラウザーで入力する https:// アドレスを正確に設定することです。
プロキシのアップロードサイズ上限も引き上げます。プロキシがリクエストボディを 1 MB に制限している場合、40 MB のスキャンは paperless に到達する前に拒否され、ブラウザーには一般的なアップロード失敗として表示されます。
consume ディレクトリの仕組み
compose ファイルは、compose ディレクトリ内の ./consume をコンテナへ bind mount します。ここに置いたファイルは取り込まれた後、フォルダーから削除されます。ファイルは paperless の管理対象として media volume 内に保存されるためです。
cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserverconsumer がファイル名を検出し、OCR を実行して、「document was added」と報告する行で処理を完了することを確認できます。1 ページのスキャンなら一連の処理は数秒で完了します。長い文書では 1 分以上かかる場合があります。
ファイルの検索方法を変更する設定が 2 つあります。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 には、デフォルトで eng が指定されている 3 文字の Tesseract コードを指定します。言語は deu+eng のようにプラス記号で連結します。Tesseract は各言語で処理を試し、最も良い結果を採用します。そのため、言語を 1 つ追加するたびに、各ページの処理に必要な CPU 時間が増えます。共有 vCPU の VPS では、スキャンの完了時間が 10 秒から 1 分になるほどの差が出ます。文書に実際に使用されている言語だけを指定してください。
このイメージには、英語、ドイツ語、イタリア語、スペイン語、フランス語が含まれています。それ以外の言語を使用する場合は、スペース区切りのリストとして PAPERLESS_OCR_LANGUAGES に追加します。たとえば PAPERLESS_OCR_LANGUAGES=tur ces のように指定してから、再起動します。コンテナは起動時に Tesseract のデータパックをダウンロードするため、この変更後の初回起動には通常より時間がかかります。
データベースとメディアをバックアップする
PostgreSQL の実行中に Docker ボリュームをコピーすると、復元できない可能性のあるバックアップになります。Paperless には専用のエクスポーターがあり、ドキュメントとすべてのメタデータを含む JSON マニフェストを ./export bind mount に書き込みます。
docker compose exec webserver document_exporter ../export --delete --no-progress-bar--delete を指定すると、現在のドキュメントに対応しなくなったエクスポート済みファイルが削除されるため、フォルダーを無制限に増加させず、ミラーとして維持できます。cron から実行する場合は --no-progress-bar を指定すると、出力を整理できます。
復元は、新しいスタック上で同じフォルダーに対して document_importer で実行します。つまり、安全に保管する必要があるのはエクスポートディレクトリだけです。VPS から暗号化・重複排除された restic バックアップを定期的にオフサイトへ送信し、restic が書き込み途中のアーカイブを取得しないよう、先にエクスポートを実行します。
export/manifest.json が存在し、ファイル数がインターフェース上のドキュメント数と一致することを確認して、バックアップを検証します。一度も一覧表示していないバックアップは、バックアップとはいえません。夜間のエクスポートが気付かないうちに失敗し始めると、さらに深刻です。そのため、cron ジョブから 自分の ntfy サーバー へ終了ステータスを送信するように設定します。これにより、復元が必要になった日ではなく、失敗した週に把握できます。
FAQ
ドメインを指定した後、すべてのページで「Bad Request (400)」が返るのはなぜですか?
Django が Host ヘッダーを拒否しています。これは、ドメインが ALLOWED_HOSTS に登録されていないためです。末尾のスラッシュを付けずに docker-compose.env の PAPERLESS_URL=https://paperless.example.com を設定し、その後 docker compose up -d を実行してコンテナを再作成してください。環境変数ファイルを編集するだけでは反映されません。実行中のコンテナは、起動時の環境を保持しているためです。
consume フォルダーに PDF を置いても何も起きません。何が問題ですか?
まず docker compose logs webserver を確認してください。権限エラーが出る場合は、USERMAP_UID と USERMAP_GID がファイルの所有アカウントと一致していません。値を修正して、コンテナを再作成してください。ログ行がまったく出ない場合は、ファイルイベントが届いていません。これは、カーネル通知がネットワーク共有を越えないために発生します。PAPERLESS_CONSUMER_POLLING_INTERVAL を 30 のような値に設定すると、paperless は代わりに 30 秒ごとにフォルダーをスキャンします。
PostgreSQL の代わりに SQLite で paperless-ngx を実行できますか?
はい。docker-compose.sqlite.yml はサポートされており、メモリ使用量も少ないため、小規模な VPS に適しています。ただし、アーカイブが増えると、全文検索やタグの一括編集が数千件の文書で明らかに遅くなります。後から移行するにはエクスポートとインポートが必要です。そのため、アーカイブが増え続ける見込みなら、最初から PostgreSQL を選択してください。
スキャンした文書のアーカイブには、実際にどの程度のディスク容量が必要ですか?
おおよそ元ファイルの 2 倍です。paperless は元ファイルを変更せずに保持し、検索可能なテキストレイヤーを含む OCR 済みの PDF を 1 つ追加で保存します。これに加えて、小さなサムネイルも保存します。テキストだけの 200 KB のスキャンなら容量は小さいままです。長い契約書をカラーでスキャンした 30 MB のファイルなら、約 60 MB を使用します。同じディスクにエクスポートディレクトリも保存する場合は、その分を加えてください。同じアーカイブで、ディスク上の使用量は 3 倍になります。
Tika と Gotenberg のコンテナは必要ですか?
PDF と一緒に Word、Excel、OpenDocument ファイルもインデックス化する場合に限り必要です。これらの形式を PDF に変換することで、paperless が OCR と検索を実行できるようにします。ただし、実行中のコンテナが 2 つ増え、数百 MB のメモリも使用します。保存するファイルがすべて PDF または画像なら、小規模なサーバーでは省略してください。