Skip to content

Paperless-ngx

Paperless-ngx turns scanned paper into a searchable archive: a document dropped into a folder is OCR’d, classified and filed, and its text becomes searchable. Five containers — the application with its workers and consumer, a database, a cache, and two converters it hands other formats to.

Last run on a host 8 Nov 2026 · data restored from a backup

Pinned at 3.4.2 (released 3 Nov 2026) · the newest upstream release

In the upstream images: 2 CRITICAL · 39 HIGH with a published fix — per image

To try it: Docker, and the OCR language packs you intend to use. See Try it locally.

To deploy it behind the proxy: Traefik running, a domain, and the OCR languages you intend to use. SMTP is optional.

On your own machine, with no proxy, domain or certificate. From apps/paperless-ngx/:

Terminal window
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs
{
echo "DB_PASSWORD=$(openssl rand -hex 32)"
echo "SECRET_KEY=$(openssl rand -base64 48 | tr -d '\n')"
echo "ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')"
} >> .env.local
mkdir -p volumes/local/redis
sudo chown 999:1000 volumes/local/redis
docker compose -f docker-compose.local.yml --env-file .env.local up -d

Then open http://localhost:8000. Stop it again with the same command and down.

It archives what you already have; it does not open or edit a document. For that, see document editing in the browser.

From apps/paperless-ngx/:

  1. Copy the environment file and set the values for this host:

    Terminal window
    cp .env.example .env
    Variable What it is
    APP_TRAEFIK_HOST the hostname Traefik routes to this stack
    PAPERLESS_OCR_LANGUAGE the language used when a document carries no language metadata
    PAPERLESS_OCR_LANGUAGES every language pack to install, space-separated (deu eng) — a comma makes one unknown package name
    USERMAP_UID / USERMAP_GID the host user that owns volumes/ — 1000:1000 on a default Debian install
    TZ the timezone timestamps are written in

    Language codes are ISO 639-2, three letters: deu, eng, fra, spa, ita, ron. The Tesseract language data lists the rest.

    Set them now, before the first document is consumed. A language pack that is not installed is not an error: Tesseract falls back to English, and the document is archived with the wrong text.

  2. Generate the two secrets and create the mail password file. tr -d '\n' is not optional — a trailing newline is part of the value, and authentication fails with a password that looks right:

    Terminal window
    mkdir -p .secrets
    openssl rand -base64 32 | tr -d '\n' > .secrets/db_pwd.txt
    openssl rand -base64 48 | tr -d '\n' > .secrets/secret_key.txt
    : > .secrets/email_pwd.txt # compose mounts it; empty while mail is off (see Mail)
  3. Create the volumes and hand them to the user the application drops to:

    Terminal window
    mkdir -p volumes/{data,media,consume,export,postgres,redis}
    sudo chown -R 1000:1000 volumes/data volumes/media volumes/consume volumes/export
    sudo chown 999:1000 volumes/redis # Redis cannot fix this ownership itself

    Use the same UID and GID you set in step 1. A mismatch surfaces as permission errors on the consume folder rather than as a startup failure.

  4. Start the stack:

    Terminal window
    docker compose up -d
  5. Watch the migrations finish:

    Terminal window
    docker compose logs paperless-app --follow

    Wait for Listening at: http://:::8000.

  6. Create the first account:

    Terminal window
    docker compose exec paperless-app createsuperuser
  7. Open https://<APP_TRAEFIK_HOST> and log in.

Terminal window
docker compose ps # five services; four healthy, tika "running" (no healthcheck)
docker compose exec paperless-app /command/with-contenv python3 manage.py check # no issues

Then put a PDF into volumes/consume/ and watch it arrive:

Terminal window
docker compose logs paperless-app --follow

The document is consumed, OCR’d and appears in the interface. Open it and read the extracted text — a document that appears with empty or wrong text means the language pack in PAPERLESS_OCR_LANGUAGES does not cover it.

The stack ships access-vpn, with limit-standard and headers-standard: reachable over your VPN, refused from everywhere else. Paperless holds whatever you scan, which is usually the most personal data on the server. Change APP_TRAEFIK_ACCESS in .env only deliberately, and consider single sign-on at the same time.

sso.yml overlays OpenID Connect. With it active, username and password login is disabled and every request redirects to Authentik.

In Authentik: an application with the slug paperless, an OAuth2/OIDC provider bound to it, and the redirect URI https://<APP_TRAEFIK_HOST>/accounts/oidc/<SSO_PROVIDER_ID>/login/callback/. Then in .env:

COMPOSE_FILE=docker-compose.yml,sso.yml
SSO_PROVIDER_ID=authentik
SSO_CLIENT_ID=<from authentik>
SSO_CLIENT_SECRET=<from authentik>
SSO_SERVER_URL=https://auth.example.com/application/o/paperless/.well-known/openid-configuration

SSO_CLIENT_SECRET lives in .env rather than in .secrets/. Paperless builds its provider configuration as a single JSON environment variable and has no _FILE form for a value inside it.

Used for password resets, workflow notifications and document share links. Mail ships off: Paperless treats it as off only while PAPERLESS_EMAIL_HOST=localhost and PAPERLESS_EMAIL_HOST_USER is empty. An empty host does not turn it off — the password reset then answers 500.

To switch it on, write the password to a file and set the relay in .env:

Terminal window
echo -n 'your-smtp-password' > .secrets/email_pwd.txt
PAPERLESS_EMAIL_HOST=smtp.example.com
PAPERLESS_EMAIL_PORT=587
PAPERLESS_EMAIL_USE_TLS=true
PAPERLESS_EMAIL_FROM=Paperless <[email protected]>

Port 465 instead: PAPERLESS_EMAIL_PORT=465 with PAPERLESS_EMAIL_USE_TLS=false and PAPERLESS_EMAIL_USE_SSL=true — never both; with USE_TLS=false alone the connection to 465 is closed.

Database PostgreSQL, container paperless-db, database paperless, user paperless_user
Password .secrets/db_pwd.txt
The archive volumes/media — the documents themselves
Also needed .secrets/secret_key.txt, or existing sessions and tokens stop working
Rebuildable volumes/data (search index) · volumes/redis · volumes/consume · volumes/export

For borgmatic:

postgresql_databases:
- name: paperless
username: paperless_user
# The server's own client, inside its container
pg_dump_command: docker exec paperless-db pg_dump
pg_restore_command: docker exec -i paperless-db pg_restore
psql_command: docker exec -i paperless-db psql

No quiescing — the dump is consistent on its own.

volumes/media is the archive. The database holds tags, correspondents and the extracted text; the documents are files on disk. A database restored without media leaves every record pointing at a document that is gone.

Restore order: database, then media, then the application. The search index rebuilds on first start; until it finishes, search returns nothing while every document is present.

Paperless also ships document_exporter, which writes documents and metadata into one self-describing tree. That is the path to use when moving between major versions rather than restoring in place.

Paperless runs schema migrations on every minor bump, and the classifier model may need rebuilding.

  1. Read the release notes for breaking changes.

  2. Export, then dump — the exporter knows what belongs together:

    Terminal window
    docker compose exec -u paperless paperless-app \
    document_exporter /usr/src/paperless/export --compare-checksums --use-filename-format
    tar czf paperless-export-$(date +%Y%m%d).tgz volumes/export volumes/media
    docker compose exec db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' \
    > paperless-db-$(date +%Y%m%d).sql
  3. Raise APP_TAG in .env, then:

    Terminal window
    docker compose pull && docker compose up -d
    docker compose logs paperless-app --follow

Permission errors on the consume or media folder. USERMAP_UID and USERMAP_GID do not match the host user that owns volumes/. Correct the values in .env or the ownership on disk, then recreate the app container.

Login is refused with a password that is definitely right. A secret file has a trailing newline. Paperless compares "password\n" against "password". Regenerate with tr -d '\n'.

A document is archived with wrong or garbled text. Its language is not in PAPERLESS_OCR_LANGUAGES, so Tesseract read it as English. Add the language, recreate the app container, and re-run OCR on the affected documents.

The first OCR of a large document takes minutes. Tesseract loads language data on first use; subsequent documents in the same language are fast.

Adding user: to the app service breaks startup. Paperless uses s6-overlay, which needs root to set up /run before it drops privileges. The blueprint leaves the directive off for that reason.