Paperless-ngx for VPS: How to Self-Host Documents
Learn how to run paperless-ngx for VPS with Docker Compose, official Postgres, PAPERLESS_URL, OCR languages, HTTPS, consume folder, and backups.
Wetin you dey build
Paperless-ngx for VPS fit turn folder of scanned paper into archive wey you fit search. You go drop PDF inside directory wey server dey watch. Server go run OCR (optical character recognition) for am, extract the text, guess date and correspondent, then file am. The installation na one Docker Compose file with four services. Everything after that na configuration. This guide spend most of the length for there because na configuration dey make installation fail. E no be photo library. OCR and correspondent guessing no go help folder wey full of holiday JPEGs. Put those ones for photo server wey dem build for am and keep Paperless for paper. Video follow the same rule. Shelf of ripped films belong for media server, where something like Jellyfin front end wey resemble 90s rental store make browsing important instead of searching.
Paperless-ngx na community fork wey dem still dey maintain from the original Paperless project. E free, you fit host am by yourself, and e dey store your documents as plain files for disk. So you no go ever lose access to your own archive. If you run am for VPS instead of home box, your scans fit reach from anywhere without opening port for your home router. E also work well with private Nextcloud instance for files wey no be paper. The same thing apply to desktop wey your scanner connect to. Your own RustDesk relay for that VPS go let you control the machine from another place without opening hole for the router too.
Wetín the stack actually dey run
The official compose file dey start four containers, and if you know wetin each one dey do, the logs go easy to understand.
webserver: na the paperless-ngx image itself. E dey run the web interface, the API, the consumer wey dey watch your input folder, and the Celery task workers wey dey do OCR.db: na PostgreSQL. E dey keep metadata, tags, correspondents, and the full-text search index tables. E no dey keep your PDFs.broker: na Valkey, a Redis-compatible key-value store. E dey serve as the task queue between the web process and the workers.gotenbergandtika: optional ones, and dem dey appear only for the-tikacompose variants. Dem dey convert Office documents (.docx,.xlsx,.odt) to PDF so paperless fit index dem.
As of October 2026, the postgres compose file dey pin docker.io/library/postgres:18 and docker.io/valkey/valkey:9-alpine, and e dey pull the app from ghcr.io/paperless-ngx/paperless-ngx:latest.
Prerequisites
- Ubuntu 24.04 KVM VPS wey get sudo access, plus Docker and Compose plugin wey you don install already. If this part still new to you, start with Docker Compose fundamentals for VPS and come back.
- Domain name wey get A record pointing to the VPS. Paperless no go accept sign-in through HTTPS for hostname wey you never tell am about, so this matter earlier than you fit expect.
- Memory na the real limit. PostgreSQL, Valkey, Granian web server, and one Tesseract OCR worker fit all dey memory together inside 2 GB for light use. Give am 4 GB if you plan import backlog of hundreds of scans, because OCR for large multi-page PDF na the memory spike wey fit make kernel out-of-memory killer kill worker.
- Disk: your archive dey stored two times: the original file and an OCR'd archive PDF. So, plan for roughly twice the size of your scans.
Get the official compose files
Interactive installer dey available:
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"E dey ask questions and write the files for you. If you do am by hand, na four commands, and you go know where everything dey. Na this you want for server wey you go maintain.
mkdir -p ~/paperless && cd ~/paperless
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
curl -fsSL -o docker-compose.env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.env
curl -fsSL -o .env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.envThe variants dey inside the same directory: docker-compose.sqlite.yml, docker-compose.mariadb.yml, and one -tika version for each. Choose postgres for new installation. SQLite dey okay for a few hundred documents, but full-text search index go slow well before PostgreSQL.
The .env file get one line, COMPOSE_PROJECT_NAME=paperless. That name go become prefix for every container and volume. So no delete am, then come dey wonder why docker compose down -v no fit find your data.
Configure docker-compose.env before the first start
Two settings no be optional. Generate the secret key with the command wey the project document:
python3 -c "import secrets; print(secrets.token_urlsafe(64))"Then edit docker-compose.env:
PAPERLESS_SECRET_KEY=<the long string you just generated>
PAPERLESS_URL=https://paperless.example.com
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu+eng
USERMAP_UID=1000
USERMAP_GID=1000PAPERLESS_SECRET_KEY dey come as the literal value change-me. E dey sign session tokens, and current releases dey treat that placeholder as unset: paperless no go start while the key dey missing, blank or still change-me, so the stack no go come up until you replace am. Set am before the first start, because if you change am later, e go log every user out.
PAPERLESS_URL na the one wey fit save you one hour. Paperless na Django application, and Django dey check both the Host header and, for sign-in form, the origin wey browser report. Set PAPERLESS_URL and e go add your address to ALLOWED_HOSTS, CORS_ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS for you. If you leave am empty and point a domain to the box, the sign-in page still go load, because PAPERLESS_ALLOWED_HOSTS dey default to *, but submitting am over HTTPS go fail with CSRF verification failed. Request aborted. because your domain no be trusted origin. Write am without trailing slash and without path.
USERMAP_UID and USERMAP_GID set the user wey the container dey run as. Match dem with your own account, as you fit check with id -u and id -g. If dem no match, files wey you copy into the consume folder no go readable by the consumer, and the log go show permission error instead of import.
Start the stack and create the first user
docker compose pull
docker compose up -d
docker compose run --rm webserver createsuperuser
docker compose logs -f webservercreatesuperuser go ask for username, email, and password. No default login dey. If you skip this step, web interface go ask the first person wey open am to create the superuser. If port 8000 still open to internet, that person fit no be you. So create the account from shell immediately. Wait for the log line wey report say server dey listen on port 8000 before you try browser. The first start also dey run database migrations, and this fit take one or two minutes.
Check am locally before you involve domain:
curl -I http://127.0.0.1:8000A 302 redirect to /accounts/login/ mean say the stack dey healthy.
Put HTTPS for front
The default compose file dey publish 8000:8000, and e bind to every interface. For public VPS, anybody wey find the address fit access your whole document archive through plain HTTP. Change the port line make e bind to loopback only:
ports:
- "127.0.0.1:8000:8000"Then terminate TLS (transport layer security) for reverse proxy and forward request go 127.0.0.1:8000. If na only this app dey run for the server, any proxy wey get ACME (automatic certificate management environment) client go work. If you dey run plenty containers behind one certificate setup, follow the Traefik reverse proxy pattern for multiple Docker Compose apps and attach webserver service to the proxy network without publishing any port.
Any proxy wey you use, the origin check for the login form na where HTTPS setup dey fail. Paperless ignore X-Forwarded-Proto unless you set PAPERLESS_PROXY_SSL_HEADER, and e dey off by default. Because of this, Django treat every proxied request as plain HTTP, and the https:// origin wey your browser send go only pass if you list am as trusted. Set PAPERLESS_URL to the exact https:// address wey you type for the browser; na this one go put am for the trusted list. If you no set am, you go see CSRF verification failed. Request aborted. for page wey look correct.
Also increase the proxy upload size limit. If proxy limit request body to 1 MB, e go reject 40 MB scan before paperless even see am, and browser go show general upload failure.
How the consume directory dey work
The compose file bind-mounts ./consume from the compose directory enter the container. Anything wey you put there go import, then e go delete am from the folder, because the file don dey inside the media volume under paperless management.
cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserverYou suppose see the consumer pick the filename, run OCR, and finish with one line wey report say dem don add the document. The whole cycle na seconds for one-page scan, but e fit take one minute or more for long document.
Two settings dey change how paperless dey find files. PAPERLESS_CONSUMER_RECURSIVE=true make paperless check subfolders, while PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true turn each subfolder name to tag. So if you drop file inside consume/invoices/2026/, e go tag am invoices and 2026. Na the cheapest filing system you fit ever build.
Detection na the other half. By default, PAPERLESS_CONSUMER_POLLING_INTERVAL na 0. This mean say paperless dey use native filesystem notifications, wey dey happen immediately. Those notifications no reliable for network filesystem. If your consume folder na NFS or SMB share wey network scanner dey write to, files fit remain there without detection. To fix am, set the interval to positive number of seconds so paperless go scan the folder instead.
OCR languages, and wetin dem cost
PAPERLESS_OCR_LANGUAGE dey take three-letter Tesseract code, and eng na the default. You fit join languages with plus sign, like deu+eng. Tesseract go then use every language wey you list for every page, so each extra language go add CPU time to every scan. For VPS wey share vCPU, na this fit make scan finish for ten seconds instead of one minute. List only the languages wey your documents actually use.
The image come with English, German, Italian, Spanish and French. For any other language, add am to PAPERLESS_OCR_LANGUAGES as space-separated list, like PAPERLESS_OCR_LANGUAGES=tur ces, then restart. The container go download the Tesseract data packs when e start, so the first boot after this change go slow pass.
Back up the database and the media
If you copy Docker volumes while PostgreSQL dey run, the backup fit no restore. Paperless get im own exporter, wey dey write documents plus one JSON manifest of all metadata inside the ./export bind mount:
docker compose exec -T webserver document_exporter ../export --delete --no-progress-bar--delete dey remove exported files wey no longer match any current document, so the folder go remain a mirror instead of growing forever. --no-progress-bar dey keep the output clean when cron dey run am, and -T dey stop Compose from asking for terminal wey cron no get.
To restore, use document_importer against that same folder for one fresh stack. This mean say na only the export directory you need keep safe. Send am offsite on schedule with encrypted, deduplicated restic backups from your VPS, and run the export first so restic no capture archive wey never finish writing.
Verify backup by checking say export/manifest.json dey exist and say the file count match your document count for the interface. Backup wey you never list no be backup. Nightly export wey quietly start failing worse pass that, so make cron job push im exit status to your own ntfy server. Then you go know the week e break instead of the day you need restore.
FAQ
Why every page dey return "Bad Request (400)" after I point my domain to am?
With the stock files, e no suppose happen, because PAPERLESS_ALLOWED_HOSTS default na *. A 400 with DisallowedHost for the container log mean say PAPERLESS_ALLOWED_HOSTS don set to a list wey no include your domain. Set PAPERLESS_URL=https://paperless.example.com for docker-compose.env, without trailing slash, and paperless go add that hostname to the list, or remove the PAPERLESS_ALLOWED_HOSTS line. Then run docker compose up -d to recreate the container. Editing the env file alone no dey do anything, because the running container keep the environment wey e start with.
I drop PDF for the consume folder but nothing happen. Wetin be the problem?
Check docker compose logs webserver first. Permission error mean say USERMAP_UID and USERMAP_GID no match the account wey own the file, so fix dem and recreate the container. If no log line dey at all, the file event no arrive. This dey happen for network shares because native filesystem notifications no reliable there. Set PAPERLESS_CONSUMER_POLLING_INTERVAL to something like 30, and paperless go scan the folder every 30 seconds instead.
I fit run paperless-ngx with SQLite instead of PostgreSQL?
Yes, docker-compose.sqlite.yml dey supported and e dey use less memory, so e fit small VPS well. The tradeoff go show as your archive dey grow: full-text search and bulk tag edits go slow down noticeably when documents reach thousands. If you migrate later, you go need export and import. So choose PostgreSQL now if you expect the archive to keep growing.
How much disk archive of scans really need?
Roughly twice the size of your source files. Paperless dey keep the original untouched and store another OCR'd PDF with searchable text layer, plus small thumbnails. A 200 KB text-only scan go remain small. A 30 MB colour scan of long contract go store about 60 MB. Add the export directory if you keep am for the same disk, and the same archive go take three times the disk space.
I need Tika and Gotenberg containers?
Only if you want Word, Excel, or OpenDocument files indexed together with your PDFs. Dem dey convert those formats to PDF so paperless fit OCR and search dem. Dem also add two more running containers and few hundred megabytes of memory. So skip dem for small box if everything wey you file already be PDF or image.