Deployment guide

Host Paperless-ngx on Your Own VPS: Complete Guide 2026

Deploy on a VPS Cloud →

Host Paperless-ngx on Your Own VPS: Complete Guide 2026

Self-hosting17 min read

Paperless-ngx turns your scanned documents into a searchable archive. This guide covers installation, OCR configuration, common date and timeout issues on entry-level VPS, and the migration procedure from the 2.x branch to the 3.x series with its known incompatibilities.

Contents· Why Paperless-ngx for Your Self-Hosted DMS1/9
  1. 01Why Paperless-ngx for Your Self-Hosted DMS
  2. 02Requirements and VPS Selection
  3. 03Installation with Docker Compose
  4. 04Multilingual OCR Configuration
  5. 05Document Date Management: The Problem and the Solution
  6. 06Resolving Timeouts on Entry-Level VPS
  7. 07Upgrading from 2.x to 3.x
  8. 08Automatic Backup of Paperless-ngx
  9. 09Updates and Maintenance

Why Paperless-ngx for Your Self-Hosted DMS

Paperless-ngx is the most active community fork of Paperless — a document management system (DMS) that indexes your PDFs and scanned images, extracts text via OCR, and lets you find them by keyword, date, correspondent or tag.

Compared to alternatives (Mayan EDMS, OpenDocMan), Paperless-ngx stands out for its ease of installation (Docker Compose in under 10 minutes), its Angular web interface, and its multilingual OCR support via Tesseract.

The project is community-maintained and publishes regular releases — check the current version on the GitHub releases page before installing. The current branch is the 3.x series; if you are starting from a 2.x instance already in service, read the "Upgrading from 2.x to 3.x" section below before touching your docker-compose.yml: v3 makes two previously inferred or implicit settings mandatory, and changes several default behaviours.

Typical use cases:
- Supplier invoice archive for a small business or SME
- Personal or family medical records (prescriptions, test results, letters)
- Document management for an association or property management
- Contract and lease archive for a real estate agency

Requirements and VPS Selection

Paperless-ngx is more resource-intensive than it appears, especially at import time. OCR processing of a multi-page PDF heavily loads the CPU — this is the main source of timeouts on small configurations.

Recommended minimum configuration:
- CPU: 2 vCPU (OCR processing is single-threaded per task, but several tasks can run in parallel)
- RAM: 2 GB minimum; 4 GB for comfortable use
- Storage: SSD, 20 GB minimum for the application + space for your documents
- OS: Debian 12 or Ubuntu 22.04/24.04

What causes timeouts on small VPS: PDFs scanned at high resolution (300+ DPI) or with many pages (50+) can exceed the default PAPERLESS_WORKER_TIMEOUT (1,800 seconds). The dedicated section below covers the resolution.

Installation with Docker Compose

The official recommended installation uses Docker Compose with three services: webserver (the application), broker (the task queue) and db (the PostgreSQL database).

⚠️ Download all THREE files, not just docker-compose.yml. This is the most expensive installation trap on this page, because it fails silently. The official compose file declares env_file: docker-compose.env: container settings are read from that file. The directory's .env, on the other hand, holds a single line — COMPOSE_PROJECT_NAME=paperless — and is used by Docker Compose to name the project and prefix the volumes; it is not injected into the containers. Writing your PAPERLESS_* variables into .env produces no error at all: the container starts, the interface responds, and none of your settings are applied. OCR then falls back to English — exactly the trap the next section describes.

# Working directory
mkdir -p /opt/paperless && cd /opt/paperless

# The THREE official files
BASE=https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose
curl -fsSL $BASE/docker-compose.postgres.yml -o docker-compose.yml
curl -fsSL $BASE/docker-compose.env          -o docker-compose.env
curl -fsSL $BASE/.env                        -o .env

# Generate the key BEFORE writing anything: a QUOTED heredoc (<<'EOF') performs no
# substitution, the line would be written literally and the "secret" would be public.
SECRET_KEY=$(openssl rand -hex 32)

# The template ships PAPERLESS_SECRET_KEY=change-me: REPLACE it, do not duplicate it.
sed -i "s|^PAPERLESS_SECRET_KEY=.*|PAPERLESS_SECRET_KEY=$SECRET_KEY|" docker-compose.env

# The remaining settings go into the SAME file (unquoted heredoc: $VAR is substituted)
cat >> docker-compose.env <<EOF
PAPERLESS_URL=https://paperless.your-domain.com
PAPERLESS_TIME_ZONE=Europe/Paris
PAPERLESS_OCR_LANGUAGE=fra+eng+ara
PAPERLESS_OCR_LANGUAGES=fra ara
EOF

docker compose pull
docker compose up -d

PAPERLESS_DBENGINE, PAPERLESS_DBHOST and PAPERLESS_REDIS are already set in the environment: block of the official compose file: do not redeclare them in docker-compose.env. If you write your own compose file, however, PAPERLESS_DBENGINE=postgresql is mandatory as of v3 (see the migration section): without it, Paperless starts on SQLite.

The interface is accessible on port 8000 after about 60 seconds of startup, and prompts you to create the administrator account on first access. If you prefer the command line — scripted installation, or recovering access later:

docker compose exec webserver createsuperuser

Then configure your reverse proxy (nginx or Traefik) to expose the service with TLS. If you put one in front of Paperless, read the PAPERLESS_TRUSTED_PROXIES paragraph in the migration section: on v3, a misdeclared proxy makes login fail with 403.

Multilingual OCR Configuration

OCR is the core of Paperless-ngx. Its configuration determines the quality of indexing and the ability to find your documents.

Available Tesseract languages: the PAPERLESS_OCR_LANGUAGE parameter accepts Tesseract language codes separated by +. For a French/Moroccan document set:

PAPERLESS_OCR_LANGUAGE=fra+ara+eng   # languages used for OCR
PAPERLESS_OCR_LANGUAGES=fra ara      # Tesseract packages to INSTALL (space-separated)

⚠️ Both variables are required, and this is the second expensive trap on this page. PAPERLESS_OCR_LANGUAGE defaults to eng and only *selects* the language; for any language not present in the image, the documentation requires you to also set PAPERLESS_OCR_LANGUAGES (a space-separated list, not +) on Docker deployments. Without it, OCR silently falls back to English: your documents are indexed, but the French and Arabic text is unreadable — and nothing in the interface says so.

The image already ships English, German, Italian, Spanish and French: only the rest needs installing. Bear in mind too that Tesseract uses considerably more CPU time with several languages enabled — declare only what you actually scan.

OCR mode: PAPERLESS_OCR_MODE controls when OCR is applied. Four values exist — and skip, which is often quoted, is not one of them:
- auto (default): Paperless checks via pdftotext whether the PDF already carries text; if there is enough, OCR is skipped for that document, otherwise it runs normally. This is the safest option for a mixed collection
- redo: re-OCRs every page and attempts to replace existing text layers — useful when the scanner produced poor OCR. It can fail on some documents (forms); the original text is then kept
- force: rasterises the document, turns the text into an image and lays OCR on top. Works everywhere, but the file grows and the text is less sharp when zoomed
- off: never invokes OCR; for PDFs the text is extracted by pdftotext alone, and images come out with no text

⚠️ If you are arriving from a 2.x instance, skip and skip_noarchive did exist and were removed in v3. A removed value is not silently honoured: Paperless logs a warning at startup and applies the default. v3 splits into two settings what skip conflated — when to run OCR (PAPERLESS_OCR_MODE) and when to produce the PDF/A archive (PAPERLESS_ARCHIVE_FILE_GENERATION, values auto by default, always, never). The migration section gives the mapping table.

For most use cases, leave auto: it avoids reprocessing born-digital PDFs (contracts, software-generated invoices) with nothing for you to tune.

Document Date Management: The Problem and the Solution

Automatic date detection is one of Paperless-ngx's most powerful features — and one of the most frustrating when it doesn't work. By default, Paperless-ngx tries to detect the document date from its text content and its filename. Several reasons can lead to an incorrect or missing date.

Problem 1: The date is in an unrecognised format. Paperless-ngx detects common formats (DD/MM/YYYY, YYYY-MM-DD, etc.) but may miss ambiguous ones (08/09/2025: 8 September or 9 August?).

Solution: verify — rather than "configure" — the reading order. PAPERLESS_DATE_ORDER already defaults to DMY, that is day, month, year: setting it explicitly changes nothing and resolves no ambiguity. This setting only serves to depart from that default, for example for a collection of US documents:

PAPERLESS_DATE_ORDER=MDY  # MM/DD/YYYY — only set this if your documents are dated that way

For a mixed collection, no global order can be correct: it is the file naming (problem 3 below) that decides.

Problem 2: The document has multiple dates and the wrong one is chosen. For example, an invoice that mentions the service date AND the issue date — Paperless-ngx takes the first one found.

Solution: use the manual date field in the web interface to fix badly dated documents, or configure matching rules that assign a date from the filename.

Problem 3: The file creation date is used instead. When no date is found in the content, Paperless-ngx falls back to the file modification date — which can be very misleading for old documents scanned recently.

Solution: name import files with the document date (YYYY-MM-DD_document-name.pdf) — Paperless-ngx detects this format in the filename before analysing the content.

Resolving Timeouts on Entry-Level VPS

On a VPS with 1 or 2 vCPU, OCR processing of large documents can exceed the default timeout and leave the document stuck in "Pending processing" status indefinitely.

Diagnosis: check the worker logs:

docker compose logs celery --tail=50

SoftTimeLimitExceeded lines confirm a timeout.

Resolution — four levers:

1. Increase the task timeout:

PAPERLESS_WORKER_TIMEOUT=3600  # 1 hour (default: 1800 s)

⚠️ The exact name matters: PAPERLESS_WORKER_TIMEOUT. A misspelled setting is not rejected by Paperless — it is silently ignored, and the timeouts continue exactly as before, which reads wrongly as "the fix didn't work".

2. Reduce import resolution. If you scan documents yourself, 200 DPI is sufficient for quality OCR — 300 DPI doubles processing time with no perceptible improvement for standard text.

3. Limit parallel processing:

PAPERLESS_TASK_WORKERS=1        # tasks in parallel (default: 1)
PAPERLESS_THREADS_PER_WORKER=1  # pages processed in parallel within ONE document

When unset, PAPERLESS_THREADS_PER_WORKER is max(floor(core_count / PAPERLESS_TASK_WORKERS), 1). The upstream rule not to cross: the product TASK_WORKERS × THREADS_PER_WORKER must not exceed the number of cores, or the instance becomes extremely slow. Many workers = many documents in parallel; many threads = one large document processed faster.
On a 2 vCPU VPS, processing two documents at once can cause timeouts that the same volume handled sequentially would avoid.

4. Optimise PDF preprocessing:

PAPERLESS_OCR_USER_ARGS={"optimize": 1, "pdfa-image-compression": "jpeg"}

This compresses images inside PDFs before processing, reducing memory and CPU load.

Upgrading from 2.x to 3.x

The 3.x series of Paperless-ngx brings structural changes that make in-place migration mandatory, and adds prerequisites that did not exist on the 2.x branch.

The prerequisite everyone forgets: upgrading to v3 is supported only from 2.20.15. If you are running an older version, move to 2.20.15 first, and only then to 3.x. Switching the image tag directly from a 2.14 to a 3.x is not a supported path, and nothing will warn you at startup.

Export/import between versions (document_exporter then document_importer on a fresh instance) is not supported — and the upstream rule is broader than commonly believed: it holds between any two versions, not just across majors, because an export contains an exact image of the database. In practice, document_importer warns (Version mismatch: Currently 3.1.x, importing 2.20.15. Continuing, but import may fail.) then fails on KeyError: 'show_on_dashboard' — a SavedView model field that existed in 2.20.15 and no longer exists in the 3.x schema. The only safe path is to let the migrations apply to the existing database.

Recommended procedure:

1. Take a full backup before anything else (see the dedicated section below).
2. Stop the services: docker compose down.
3. Review, in docker-compose.env and docker-compose.yml, the settings that v3 makes mandatory or removes (detailed just below).
4. Point the docker-compose.yml image at the target 3.x version and restart: docker compose up -d — Django migrations apply automatically at webserver startup.
5. Monitor the logs: docker compose logs webserver --tail=100 — a failing migration prints the error and blocks startup.

PAPERLESS_SECRET_KEY becomes mandatory. There used to be a built-in default key; on v3 it must be declared, and Paperless refuses to start without it. Reusing the previous value preserves sessions and signed tokens; setting a new one invalidates them all. That is a deliberate choice, not a configuration detail.

PAPERLESS_DBENGINE becomes mandatory with PostgreSQL or MariaDB. On v2 the engine was inferred from the presence of PAPERLESS_DBHOST; on v3 it must be explicit, and the default value is sqlite. The accepted values are sqlite, postgresql and mariadb — nothing else. This is the first migration trap: without this setting, the instance starts on an empty SQLite database, your PostgreSQL is intact but nobody reads it any more, and the dashboard reports zero documents.

# v2 (PostgreSQL inferred from PAPERLESS_DBHOST)
PAPERLESS_DBHOST: db
# v3 (the engine must be explicit)
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBHOST: db

PAPERLESS_OCR_MODE=skip is gone. The skip and skip_noarchive values have been removed, and a removed variable is not silently honoured: a warning is logged at startup. Preserving v2 behaviour means splitting the intent across two now-independent settings:

# v2: skip OCR when text is present, but always archive
PAPERLESS_OCR_MODE=skip
# v3: equivalent
PAPERLESS_OCR_MODE=auto
PAPERLESS_ARCHIVE_FILE_GENERATION=always

Redis → Valkey incompatibility. As of v3, the official compose file ships Valkey as the broker (valkey/valkey:9-alpine), no longer Redis: this is not a fringe choice, it is what you get as soon as you replace your compose file with the project template. If your broker volume was created by a recent Redis version, Valkey refuses to load it and the container enters a restart loop on Can't handle RDB format version 15 (or 13, depending on the origin), while the webserver waits for a broker that never comes. The fix is to delete the broker volume before switching: it only holds queued tasks, no critical persistent data.

docker compose down
docker volume rm paperless_redisdata   # the prefix comes from COMPOSE_PROJECT_NAME
docker compose up -d

The search index rebuilds itself. v3 replaces Whoosh with Tantivy, and the format is incompatible: the index is rebuilt from scratch on first startup — which explains a slower first launch. Under Docker the container runs document_index reindex --if-needed on every start, so no manual action is required. If consumption nonetheless fails with Schema error: 'An index exists but the schema does not match.' (a case reported on an instance that had run the 3.0 beta), force a clean rebuild:

docker compose exec webserver document_index reindex --recreate

Watch the search syntax too: note: becomes notes.note: and custom_field: becomes custom_fields.value:. Saved views carrying an explicit prefix are migrated automatically, but a search without a prefix that used to match note content will no longer do so.

Three behaviour changes that catch people out. Task history is cleared during the upgrade: past, failed or acknowledged tasks will not reappear. v3 no longer rejects duplicates by default — it accepts them and lets you spot them in the interface; if you relied on that rejection, re-enable it with PAPERLESS_CONSUMER_DELETE_DUPLICATES=true. Finally, behind a reverse proxy, login rate limiting now determines the client IP differently: if login returns 403 Forbidden after the upgrade, declare the chain with PAPERLESS_TRUSTED_PROXIES, and if needed PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT — the number of hops in X-Forwarded-For, which is not necessarily the number of configured IP addresses.

About MariaDB. It remains supported (PAPERLESS_DBENGINE=mariadb), even though the project recommends PostgreSQL for new installations. The Debian 12 migration failures circulating on forums did not come from the driver: they came from bare-metal installations where the new release had been unpacked on top of the old one. Stale migration files left on disk make manage.py migrate fail with a NodeNotFoundError. The remedy is to delete the previous source tree (src/, static/) before deploying, not to change database engine — and in a Docker deployment like the one in this guide, that scenario cannot occur, since the image is replaced wholesale.

Mail after the upgrade. The command that forces a mail fetch is mail_fetcher, with no argument: it processes every configured account and rule.

docker compose exec webserver mail_fetcher

If a mail flow stops producing documents after the upgrade, look at the task error in the interface first: the reported cases pointed to the search index (the Schema error above), not to credentials. Then check that the account still holds its IMAP permissions; if you use an OAuth token, tick the box indicating that the password is in fact a token.

Before any major update, test the procedure on a copy of your environment. With Docker Compose, this means copying your /opt/paperless directory to a second VPS, pointing a test subdomain, and applying the update there. You can then verify that your documents, tags and correspondents are intact before touching production.

Automatic Backup of Paperless-ngx

Paperless-ngx manages two types of critical data: the PostgreSQL database (metadata, tags, correspondents, rules) and the document files. Both must be backed up — one without the other restores nothing usable.

Daily backup script:

#!/bin/bash
BACKUP_DIR="/backups/paperless/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# PostgreSQL dump
docker compose exec -T db pg_dump -U paperless paperless \
  | gzip > "$BACKUP_DIR/db.sql.gz"

# Native Paperless export (includes config and documents)
docker compose exec -T webserver document_exporter ../export
tar -czf "$BACKUP_DIR/export.tar.gz" /opt/paperless/export/

# Rotation — keep 14 days
find /backups/paperless -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +

echo "Backup complete: $BACKUP_DIR"

Schedule this script in cron (0 3 * * * for 3am), and verify that backups reach storage external to the VPS (rsync to an S3 bucket or another server). The -T after exec avoids the "the input device is not a TTY" error when the script runs without a terminal.

⚠️ An export does not replace a volume backup, and is not a bridge between versions: it can only be re-imported into the same version of Paperless-ngx that produced it. For disaster recovery, therefore, also keep a copy of the Docker volumes, or record the exact version alongside the export.

Updates and Maintenance

Paperless-ngx publishes regular releases. Updating is straightforward with Docker Compose:

# Pull the new image
docker compose pull

# Restart the services (database migrations apply automatically)
docker compose up -d

# Check everything is OK
docker compose logs webserver --tail=20

Before each major update: read the CHANGELOG on GitHub — major versions (v2.x → v3.x) may require additional migration steps, and sometimes a mandatory intermediate version, such as 2.20.15 before v3. The dedicated section above details the known incompatibilities of the 3.x series.

Monitoring: Paperless-ngx does not expose a /metrics endpoint of its own. What exists is Flower, the Celery task monitor, enabled by defining PAPERLESS_ENABLE_FLOWER; it is Flower that exports metrics usable by Prometheus, on top of showing tasks in progress, queued and completed. This is the right place to spot a processing job that stops silently — the symptom of an OCR timeout, precisely.

⚠️ One detail that changes after moving to v3: since task history was cleared by the migration, an empty list on first startup is not a sign of failure. It is the tasks after the upgrade that count.

Digitise and index all your documents

ServOrbit VPS Cloud provides the CPU, storage and Docker environment ready for Paperless-ngx, its Tesseract OCR, PostgreSQL and Redis. Turn your paper stacks into a searchable, backed-up archive under your sole control.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab