Paperless-ngx: OCR Every Document You Own, Searchable

Deploy Paperless-ngx to OCR every document on ingest, so you search the text of your scans and PDFs — not the filenames. Runs on a NAS or small server.

On this page
  1. What Paperless-ngx is (and what OCR buys you)
  2. Deploy Paperless-ngx
  3. The acceptance test that actually proves it works
  4. Protect the archive from day one
  5. What’s next

Somewhere in my house is the receipt for the dishwasher. I know I have it. It’s in a drawer, or a folder on some drive named scans_final_FINAL, or an email attachment from 2019. When the dishwasher broke, I spent longer looking for the receipt than the repair took. The problem was never that I didn’t have my documents — it’s that I couldn’t find the one I needed, because they were organized by “wherever I dropped them” and searchable by nothing.

Paperless-ngx is the cure, and it’s one of the most quietly life-improving things I run at home. You drop a document in, and it reads the text — even out of a photo or a scan with no text layer — and makes every word searchable. This is the third stop in the Self-Host the Apps You Actually Use series: after teaching my local AI to search the web and my bookmarks to tag themselves, this is the one my whole household actually uses.

Here’s the trick that makes it work — OCR on ingest:

A scan of pixels becomes searchable textconsume folderno text layerOCRreads the pixelsFull-text indexevery word stored“invoice#774411”found!The document went in as an image; it comes out searchable by the words printed on it.
First: make these values your own

The addresses below are examples. Replace 10.0.0.50 with your Paperless host’s real IP, and add any hostname you’ll actually use (a LAN name, a Tailscale address) to the allowed-hosts list. Choose your own admin username in place of youradmin and keep its password in your password manager, never in a note. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


What Paperless-ngx is (and what OCR buys you)

Paperless-ngx is a self-hosted document management system. The workflow is beautifully simple: you drop a file into a watched “consume” folder, and Paperless ingests it — extracting the text, guessing a title and date, and filing it with tags and document types you can define. From then on it’s searchable, taggable, and viewable from any browser.

The magic ingredient is OCR — optical character recognition. A scanned receipt or a photo of a letter is, to a computer, just a grid of colored dots; there’s no text in it to search. OCR (Paperless uses the excellent Tesseract engine under the hood) looks at those dots, recognizes the letters, and produces real, searchable text layered onto the document. That’s the difference between “I have 4,000 scans” and “I can find the one that mentions the word dishwasher.”


Deploy Paperless-ngx

Paperless runs as a small stack of containers with Docker Compose, and it’s happiest living on a NAS or a small always-on server where your files already are. The project ships an interactive install script that generates a compose setup by asking you a few questions — it’s the recommended starting point per the official setup docs.

1Run the official installer10 min

On a host with Docker and the Compose plugin, run the bootstrapper. It’ll ask about your database, broker, folders, and ports, then write out a ready-to-run compose project.

Paperless-ngx interactive installer

bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
2Choose SQLite + a Redis-compatible broker2 min

Two choices matter for a home setup. For the database, the project supports both PostgreSQL and SQLite; for a single-user household archive I pick SQLite — it’s one fewer container and backs up as a single file. For the broker (Paperless uses a task queue that needs a Redis-compatible service), the current official compose uses Valkey — the open-source Redis fork — and that’s what I run.

When to reach for PostgreSQL instead

SQLite is great for one user. If you’ll have several people ingesting documents at once, or you’re importing tens of thousands of files, PostgreSQL is the more scalable database. You can start on SQLite and migrate later.

3Pin the image — and mind the missing 'v'2 min

Leaving the image on :latest means an unattended pull could change your version out from under you. Pin it to a specific release in your compose file so upgrades are a deliberate act (read the release notes, then bump).

docker-compose.yml — pin the webserver image

image: ghcr.io/paperless-ngx/paperless-ngx:3.0.5
The GHCR tag drops the release's 'v' prefix

This one cost me a confused ten minutes. GitHub releases are named like v3.0.5, but the container tag on GHCR is 3.0.5no v. Use :v3.0.5 and you get manifest unknown. Check the releases page for the current version and drop the v when you write the tag.

4Fix the host and CSRF settings before your first login3 min

Paperless is a Django app, and Django is strict about which hostnames and origins it will answer to. If you skip this you’ll hit a login that fails with a host or CSRF error. Set both, listing every address you’ll reach it by:

docker-compose.env — allowed hosts and trusted origins

PAPERLESS_ALLOWED_HOSTS=localhost,127.0.0.1,10.0.0.50,paperless.homelab.lan
PAPERLESS_CSRF_TRUSTED_ORIGINS=http://localhost:8020,http://127.0.0.1:8020,http://10.0.0.50:8020
Django treats localhost and 127.0.0.1 as different hosts

They look like the same machine to you; to Django they’re two separate hostnames. List both, or a request that arrives as one while only the other is allowed gets a 400 Bad Request — which is easy to misread as “ingestion is broken” when your documents are actually importing fine. The configuration docs also offer a shortcut: setting PAPERLESS_URL can populate the allowed-hosts and trusted-origins together.

5Start it and create your admin user3 min
Bring up the stack

docker compose pull
docker compose up -d

Create the first superuser (the installer usually prompts for this, or you can set PAPERLESS_ADMIN_USER / PAPERLESS_ADMIN_PASSWORD in the env before first boot). Then open the web UI on your chosen port and log in.


The acceptance test that actually proves it works

It’s tempting to declare victory the moment the web UI loads. Don’t. The UI loading only proves the web server runs — it says nothing about whether OCR and search actually function end to end. Here’s the test I trust, and it’s deliberately mean: feed it an image-only document with no text layer at all, so OCR is the only way any text could exist to find.

Prove the whole chain, not just the front page1. Image-only PDFno text layer2. Forced OCRtesseract must run3. Search a wordprinted in the imagePASSmatch foundIf a word visible only as pixels becomes searchable, ingest + OCR + search are all working.

I generated a one-page PDF containing an image of an invoice number and no embedded font, dropped it in the consume folder, waited for it to ingest, and then searched for that number. It came back. That single search proves ingest fired, Tesseract OCR’d the image, and the full-text index and search all work — none of which the loading web page could tell you.


Protect the archive from day one

There’s a special kind of pain in losing the archive you finally organized. Documents are precious and irreplaceable, so the moment Paperless is live, make sure its data and media folders are inside your backup scope — ideally before you import anything you care about. I deliberately put Paperless’s storage inside an existing nightly backup set so the archive was covered the hour it was born. A black-box monitoring probe on the web UI rounds it out, so a stalled service pages you instead of silently refusing new documents. The companion playbook has the full deploy.


What’s next

You can now find any document by a word printed inside it — no more drawer-diving. Next in the series, we stop searching for updates entirely and let the homelab watch pages for us: changedetection.io polls any web page and pings your phone only when something actually changes.


Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.