LinuxstorageTested on real hardware

Deploy Paperless-ngx on a NAS (OCR Document Archive)

Stand up Paperless-ngx with Docker Compose on a NAS or small server: SQLite + Valkey, the allowed-hosts fix, and a forced-OCR acceptance probe that proves search works.

DistrosDebian 12, Ubuntu 24.04
Shellbash
Updated
Script
bash
# Deploy Paperless-ngx (OCR document management) with Docker Compose.
# Full walkthrough + the acceptance test: /blog/paperless-ngx-document-archive
# Requires Docker + Compose v2 on the host (a NAS or small always-on server).

# 1. Run the OFFICIAL interactive installer. It asks about database, broker,
#    folders and ports, then writes a ready-to-run compose project.
bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
#    - Database: SQLite is fine for a single-user archive (one fewer container,
#      single-file backups). Choose PostgreSQL if you expect many users / heavy ingest.
#    - Broker: a Redis-compatible service; the current official compose uses Valkey.

# 2. In the generated docker-compose.yml, PIN the image to a real release.
#    GHCR DROPS the release 'v' prefix: use 3.0.5, NOT v3.0.5 (v = manifest unknown).
#    Check https://github.com/paperless-ngx/paperless-ngx/releases for the current tag.
#      image: ghcr.io/paperless-ngx/paperless-ngx:3.0.5

# 3. In the generated env file, set allowed hosts + trusted origins. Django treats
#    localhost and 127.0.0.1 as DIFFERENT hosts, so list BOTH plus every real address.
cat >> docker-compose.env <<'EOF'
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
PAPERLESS_ADMIN_USER=youradmin
# Set PAPERLESS_ADMIN_PASSWORD from your secret store before first boot, not here.
EOF

# 4. Bring it up.
docker compose pull
docker compose up -d

# 5. ACCEPTANCE TEST (the honest one): feed it an IMAGE-ONLY PDF with no text
#    layer, so OCR is the only way any text can exist, then search for a word
#    printed only in the image. If it's found, ingest + OCR + search all work.
#    - Drop the image-only PDF into the consume folder (e.g. ./consume/).
#    - Wait for it to disappear (ingested), then search that word in the UI.

What this does

This deploys Paperless-ngx — a self-hosted document management system with OCR — using its official interactive installer, which generates a Docker Compose project. Once running, you drop PDFs or scanned images into a watched consume folder and Paperless runs Tesseract OCR to make the text searchable. It’s an ideal service for a NAS, where your documents and backups already live.

The full walkthrough, including the two Django gotchas and the acceptance test, is in Paperless-ngx: OCR Every Document You Own, Searchable.

Prerequisites

  • A host with Docker and the Docker Compose v2 plugin — a NAS that supports Docker, or a small Linux server.
  • Enough disk for your documents plus their OCR’d copies and thumbnails. Documents are the point; give them room.
  • Somewhere to keep the admin password — a password manager, not a note.

Notes

  • Make these values your own before you rely on the result: replace 10.0.0.50 with your Paperless host’s real IP, paperless.homelab.lan with your real hostname, youradmin with your chosen admin username, and set the admin password from your secret store (never paste a real password into the env file in a repo). Add any Tailscale or LAN address you’ll use to the allowed-hosts list. If a value looks specific to one machine, it’s a placeholder to change, not a literal to copy.
  • The GHCR tag has no v. Releases are named v3.0.5; the container tag is 3.0.5. Using :v3.0.5 yields manifest unknown. Pin a real release and re-check the releases page before bumping.
  • localhost127.0.0.1 to Django. List both in PAPERLESS_ALLOWED_HOSTS (and matching PAPERLESS_CSRF_TRUSTED_ORIGINS) or a login/POST can 400 — which is easy to misread as an ingestion failure when documents are actually importing fine. PAPERLESS_URL can set these together per the configuration docs.
  • SQLite vs PostgreSQL is a real choice. SQLite is simplest for one user and backs up as a single file; PostgreSQL scales better for many users or heavy ingestion. You can migrate later.
  • Back up data/ and media/ from day one. These hold your archive; put them in your backup scope before importing anything you can’t afford to lose.
  • Trust the forced-OCR acceptance test, not the loading UI. A web page that loads only proves the web server runs. An image-only PDF whose printed text becomes searchable proves ingest, OCR, and search all work end to end.