What runs on the machine
Paperless-ngx has three working parts: a web server for the UI and the API, a consumer that watches a folder for new files, and a Celery task processor that does the actual OCR and consumption work. The PostgreSQL compose file adds a database container and a Valkey container as the task broker; neither publishes a host port. The variant with -tika in its name adds Gotenberg and Apache Tika so Office files (.docx, .xlsx, .odt and similar) can be ingested too.
Each new document goes through the same pipeline. If it has no text layer, it is OCR'd with Tesseract through OCRmyPDF, and a PDF/A archive copy is written next to the original, which Paperless never overwrites; a born-digital PDF that already has text keeps only its original unless PAPERLESS_ARCHIVE_FILE_GENERATION is set to always. Tags, correspondents and document types are then matched automatically. PDF, PNG, JPEG, TIFF, GIF and WebP go through OCR; plain text is stored as is.
Sizing: where CPU, memory and disk go
The project publishes no minimum specification; its FAQ says it runs on a Raspberry Pi 3 B with slow OCR. On a server, the load follows three settings and one storage habit.
| Resource | What consumes it | Setting that controls it |
|---|---|---|
| CPU | OCR of each page during consumption | PAPERLESS_TASK_WORKERS (default 1) × PAPERLESS_THREADS_PER_WORKER must not exceed the core count |
| CPU | Several OCR languages at once (deu+eng) | PAPERLESS_OCR_LANGUAGE; the docs warn it costs much more CPU time |
| Memory | Each web worker loads the whole application | PAPERLESS_WEBSERVER_WORKERS (default 1) |
| Disk | Originals, a PDF/A archive copy of each scan, thumbnails, database | PAPERLESS_ARCHIVE_FILE_GENERATION: auto (default) archives scans only, always adds text PDFs, never skips archives |
Our own planning rule, not the project's: give the library disk room for the originals, the archive copies of scans and one full export side by side, because the exporter writes a complete copy before you ship it elsewhere. Back-scanning a filing cabinet is the one time the CPU is busy for hours; after that a household or small office trickles in a few pages a day.
Which machine to put it on
Paperless-ngx is a single service with its database on the same box, which is the job the dedicated server range describes for its Moonshot cartridges and single-socket lines. A 4-core Xeon E3 cartridge with NVMe handles a personal archive; the 16-core Xeon D cartridge with 1.92 TB of NVMe lets you raise the worker count for a large back-scan. Every machine is the whole box with root, and nothing else runs on it unless you put it there.
Disks are the real decision. The rack and blade lines have a hardware RAID controller, so the library can sit on a mirrored pair; no cartridge has a hardware RAID controller, and most have a single disk (the NVMe cartridges, the m510 and m710x among them, can mirror only in software), which makes the off-machine export below mandatory rather than optional. Pick the disk layout in the build-to-order configurator. Rack servers also have an out-of-band console, which gets you back in if a firewall rule locks out SSH.
The port is unmetered and inbound transfer is not billed, so loading an existing scan library onto the server, or pulling a full export off it every night, adds nothing to the invoice.
Installing with Docker Compose
- The machine is handed over with a root login. Create your own user with sudo rights, give it the SSH key root uses (or paste your public key into its
authorized_keysif root logs in by password), confirm you can log in as that user, and do everything after this as that user. - Install Docker Engine and the Compose plugin on Ubuntu or Debian. Turn ufw on with SSH allowed before starting any container.
- Download the PostgreSQL compose file (saved as
docker-compose.yml),docker-compose.envand.envfrom the project'sdocker/composefolder. PostgreSQL is what the docs recommend for new installs. - Change the published port from
8000:8000to127.0.0.1:8000:8000. Docker writes its own iptables rules and ufw does not filter published ports; with a host address in front, Docker binds IPv4 loopback only and publishes nothing on0.0.0.0or[::]. - Set
PAPERLESS_SECRET_KEY. The env file ships the placeholderchange-me; the docs say the key signs sessions and tokens and must be a long random string. The command below writes one generated with Python'ssecretsmodule straight into the file, so the key never appears in the process list or shell history. Thenchmod 600the file. - In the same file,
PAPERLESS_TIME_ZONEandPAPERLESS_OCR_LANGUAGEship commented out: delete the leading # and give them values, for exampleEurope/Bucharestandeng. Runid -uandid -gas your user; if either is not 1000, uncomment the two USERMAP lines and put those numbers in them so the container can write toconsumeandexport. - Pull and start. The first start runs the database migrations;
sudo docker compose logs -f webservershows when it is listening.
# as root, once: your own sudo user, with the SSH key root logs in with
adduser you
usermod -aG sudo you
install -d -m 700 -o you -g you /home/you/.ssh
install -m 600 -o you -g you /root/.ssh/authorized_keys /home/you/.ssh/
# test ssh you@your-server in a new window, then continue as you
sudo ufw allow OpenSSH
sudo ufw enable
mkdir -p ~/paperless/consume ~/paperless/export && cd ~/paperless
BASE=https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose
curl -fsSL -o docker-compose.yml $BASE/docker-compose.postgres.yml
curl -fsSL -O $BASE/docker-compose.env
curl -fsSL -O $BASE/.env
# web port on IPv4 loopback only: nothing on 0.0.0.0 or [::]
sed -i 's|"8000:8000"|"127.0.0.1:8000:8000"|' docker-compose.yml
# random PAPERLESS_SECRET_KEY, written by Python so it never sits in argv or history
python3 -c "import secrets,pathlib; p=pathlib.Path('docker-compose.env'); p.write_text(p.read_text().replace('PAPERLESS_SECRET_KEY=change-me','PAPERLESS_SECRET_KEY='+secrets.token_urlsafe(64)))"
grep -c 'change-me' docker-compose.env # expect 0
chmod 600 docker-compose.env
sudo docker compose pull
sudo docker compose up -d
The first superuser, created before anyone can reach it
On a fresh install, the first browser session to reach the UI is asked to create a superuser, and a superuser sees every document. So the account is made while port 8000 answers on loopback only. Check the listener, then create it from the command line with Django's createsuperuser, which prompts for the password instead of taking it as an argument.
# on the server, in ~/paperless
sudo ss -ltnp | grep ':8000' # expect 127.0.0.1:8000 and nothing else
sudo docker compose exec webserver createsuperuser
# on your own computer, through your own SSH login
ssh -N -L 8000:127.0.0.1:8000 you@your-server
# then browse to http://localhost:8000
The docs suggest a separate everyday account without superuser rights. Create it under Users & Groups, and turn on two-factor authentication from My Profile for both.
Feeding it documents on a remote box
Three routes work without opening anything new: upload in the web UI (drag a file anywhere into the app), the mail consumer configured under /settings/mail, or copying files over SSH into ~/paperless/consume. The docs mention FTP or a Samba share for scanners that push files; on a machine with a public address, keep that kind of share behind a VPN rather than on the internet. Files are removed from consume once ingested, so never treat it as storage.
Exports, updates and going public
The supported backup is document_exporter: documents, thumbnails and a manifest.json with the database contents, written to the export folder that the compose file mounts. Re-running it updates the existing export with changed files only, so rsync from another machine stays incremental. Under cron, use exec -T (no TTY) and --no-progress-bar; never -it.
# /etc/cron.d/paperless-export (root, 02:30 daily)
30 2 * * * root cd /home/you/paperless && docker compose exec -T webserver document_exporter ../export --no-progress-bar
# from a second machine, pull the export over SSH
rsync -a --delete you@your-server:paperless/export/ /srv/backup/paperless/
- An export only imports into the same Paperless-ngx version; note the version beside each copy. API tokens are not exported and must be re-created.
- Update after a fresh export:
docker compose down,docker compose pull, thendocker compose up -d. Read the release notes first; v3 has its own migration guide. - To publish it, put a TLS reverse proxy on 80/443 in front of
127.0.0.1:8000, allow those two ports in ufw, and setPAPERLESS_URL=https://docs.example.comwith no trailing slash. That limits the accepted host names to that domain pluslocalhost, so through the tunnel keep usinghttp://localhost:8000. Port 8000 itself stays on loopback.
When a dedicated server is the wrong place
One person with a few hundred documents and no back-scan does not need a whole machine; a Cloud VPS runs the same compose stack. Paperless-ngx is also one instance with one database, not a cluster: if it must survive a disk failure without a restore, choose a mirrored rack line and keep the nightly export somewhere else. For a password vault next to it, the Vaultwarden page follows the same loopback-first pattern.
Questions
Will Paperless-ngx start without PAPERLESS_SECRET_KEY?
No. The configuration docs mark it required, with no default, and Paperless refuses to start without it. The shipped docker-compose.env contains the placeholder change-me, which is a value, so replace it with a long random string before the first start.
Is SQLite good enough, or should it be PostgreSQL?
Both work. The setup docs recommend PostgreSQL for new installations, and the PostgreSQL compose file is the one used on this page. SQLite saves resources on very small hardware.
Does OCR need a GPU?
No. OCR runs on the CPU through OCRmyPDF and Tesseract, and the documentation lists no GPU. Throughput comes from cores, which is why the worker and thread settings matter more than any graphics card.
Can it read Word and Excel files?
Yes, with the compose file whose name contains -tika. It adds Tika and Gotenberg containers; without them Paperless-ngx handles PDFs, images and plain text.
Where are the documents stored inside Docker?
In the paperless_media volume, usually under /var/lib/docker/volumes/paperless_media/_data on Linux. The FAQ says not to change permissions or move files there by hand; use the exporter to get files out.
How do I add a second admin later?
Run createsuperuser again with docker compose exec, or grant superuser status from an existing superuser account. Superuser status can only be granted by another superuser.