SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-28

How to Self-Host Paperless-ngx for VPS with Docker

Run paperless-ngx for VPS with official Postgres Docker Compose stack, set PAPERLESS_URL, OCR languages and HTTPS, then back up your consume folder and archive.

Wetin you dey build

Paperless-ngx for VPS fit turn folder of scanned paper into searchable archive. You go drop PDF inside watched directory, server go run OCR (optical character recognition) on 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, and this guide spend most of the length on am, because na there installations dey fail. E no be photo library: OCR and correspondent guessing no dey useful for folder of holiday JPEGs, so put dem inside photo server wey dem build for am and use paperless for paper.

Paperless-ngx na community fork wey people still dey maintain for the original Paperless project. E free, you fit self-host am, and e dey store your documents as plain files for disk, so nobody fit lock you out of your own archive. If you run am for VPS instead of home box, you fit reach your scans from anywhere without opening port for your home router, and e work well with private Nextcloud instance for files wey no be paper. The same logic apply to the desktop wey your scanner dey connected to, because RustDesk relay wey you own for that VPS go let you control that machine from another place without opening hole for the router too.

Wetin the stack really dey run

The official compose file dey start four containers, and if you know wetin each one dey do, logs go easier to understand.

  • webserver: na the paperless-ngx image itself. E dey run the web interface, the API, the consumer wey dey monitor your input folder, and the Celery task workers wey dey do OCR.
  • db: PostgreSQL. E dey store metadata, tags, correspondents, and the full-text search index tables. E no dey store your PDFs.
  • broker: Valkey, na Redis-compatible key-value store. E be the task queue between the web process and the workers.
  • gotenberg and tika: optional; dem dey only for the -tika compose variants. Dem convert Office documents (.docx, .xlsx, .odt) to PDF so paperless fit index dem.

As of July 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 you get sudo access to, plus Docker and Compose plugin wey don already install. 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 serve for hostname wey you never tell am about, so this matter important earlier than you fit expect.
  • Memory na the real constraint. PostgreSQL, Valkey, gunicorn and one Tesseract OCR worker fit all dey resident at once 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 OCR'd archive PDF. So plan roughly double 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/.env

The 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

Settings two 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=1000

PAPERLESS_SECRET_KEY dey come as the literal value change-me. E dey sign session cookies, so if you leave am, anybody wey know the default fit forge session. Set am before the first start, because if you change am later, e go log every user out.

PAPERLESS_URL na the one wey go save you one hour. Paperless na Django application, and Django dey validate the Host header for every request. Set PAPERLESS_URL and e go fill ALLOWED_HOSTS, CORS_ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS for you. If you leave am empty, point domain to the box, and every page go return Bad Request (400), while DisallowedHost go show for the container log. 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, after you check am with id -u and id -g. If dem no match, files wey you copy enter 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 webserver

createsuperuser dey ask for username, email, and password. No default login dey, so if you skip this step, you go reach sign-in page wey no go accept anything. Wait for log line wey report say server dey listen on port 8000 before you try am for browser. The very first start still dey run database migrations, and e fit take one or two minutes.

Check am locally before you involve domain:

curl -I http://127.0.0.1:8000

A 302 redirect go /accounts/login/ mean say the stack dey healthy.

Put HTTPS for front

The stock compose file dey publish 8000:8000, wey bind to every interface. For public VPS, this one dey serve your whole document archive through plain HTTP to anybody wey find the address. 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 several containers behind one certificate setup, follow Traefik reverse proxy pattern for multiple Docker Compose apps and attach webserver service to the proxy network without publishing any port at all.

Any proxy wey you use must send X-Forwarded-Proto: https. Without am, Django go believe say request come through HTTP, the origin check for login form go fail, and you go get CSRF verification failed. Request aborted. for page wey look correct. The other part of this fix na to set PAPERLESS_URL to the exact https:// address wey you type for browser.

Also increase the proxy upload size limit. If 40 MB scan pass through proxy wey limit request body to 1 MB, proxy go reject am before paperless ever see am, and browser go show general upload failure.

How consume directory dey work

The compose file bind-mounts ./consume from the compose directory enter the container. Anything wey you put there, paperless go import am and then 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 webserver

You suppose see 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 wey you fit ever build.

Detection na the other half. By default, PAPERLESS_CONSUMER_POLLING_INTERVAL na 0. This mean say paperless dey use kernel filesystem notifications, and dem dey happen immediately. Those notifications no dey cross network filesystem. If your consume folder na NFS or SMB share wey network scanner dey write to, paperless no go detect anything. 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, eng by default. Join languages with plus sign, like deu+eng. Tesseract go try each one and keep the best result, so every extra language go multiply the CPU time wey e spend for every page. For VPS wey share vCPU, na this fit make scan finish for ten seconds instead of one minute. List only the languages wey dey the documents you really dey process.

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 you make this change go slow.

Database and media back up

PostgreSQL dey run while you copy Docker volumes fit give you backup wey no go restore. Paperless get im own exporter, wey dey write documents plus JSON manifest of all metadata inside the ./export bind mount:

docker compose exec webserver document_exporter ../export --delete --no-progress-bar

--delete dey remove exported files wey no longer match current document, so the folder go remain mirror instead of growing forever. --no-progress-bar dey keep the output clean when cron dey run am.

To restore, use document_importer against that same folder for fresh stack. This mean say na only the export directory you need keep safe. Send am go offsite on schedule with encrypted, deduplicated restic backups from your VPS, and run the export first so restic no go 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 start fail quietly worse pass that one, so make cron job push its exit status go your own ntfy server. This way, you go know the week e break instead of the day wey you need restore.

FAQ

Why every page dey return "Bad Request (400)" after I point my domain to am?

Django reject the Host header because your domain no dey inside ALLOWED_HOSTS. Set PAPERLESS_URL=https://paperless.example.com for docker-compose.env, without trailing slash, then run docker compose up -d to recreate the container. Editing the env file alone no go do anything, because the running container keep the environment wey e start with.

I drop PDF for the consume folder and nothing happen. Wetin be wrong?

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 show at all, the file event never arrive. This dey happen for network shares because kernel notifications no dey cross dem. 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 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. Migration later mean export and import, so choose PostgreSQL now if you expect the archive to keep growing.

How much disk space archive of scans really need?

Roughly twice the size of your source files. Paperless keep the original unchanged and store another OCR'd PDF with searchable text layer, plus small thumbnails. A 200 KB text-only scan 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 occupy 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 convert those formats to PDF so paperless fit OCR and search dem. Dem also add two more running containers and some hundred megabytes of memory, so skip dem for small box if everything wey you file already be PDF or image.

#paperless-ngx#documents#self-hosting#docker#ocr