How to Deploy Paperless-ngx on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host Paperless-ngx on a VPS with Docker Compose and PostgreSQL — OCR'd, tagged and searchable paperwork, with an HTTPS front door and a backup you can actually restore.
- A VPS with 2 GB RAM or more — OCR is the hungry part
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
What Paperless-ngx is
Paperless-ngx is a document management system: you feed it scans, PDFs and photos of paper, and it runs OCR on each one, stores the original plus a searchable archive copy, and indexes the text so you can find a 2019 insurance letter by typing a word that appears in it. Documents get tags, correspondents and document types, and Paperless learns to assign those automatically from the ones you set by hand.
It is a Python / Django application under the GPL-3.0 licence, and it runs as a small stack: the web server (which also runs the background consumer and task workers), a PostgreSQL database and a Valkey (Redis-compatible) broker.
The part to take seriously before you start is what goes into it. A Paperless instance ends up holding tax returns, contracts, medical letters and ID scans. That is why this guide binds it to the loopback interface, puts HTTPS in front, and spends real time on backups.
Server sizing
The catalog lists 2 GB of RAM as the practical minimum for Paperless-ngx. Idle, the stack is modest; the load arrives when documents are consumed, because OCR (Tesseract, via OCRmyPDF) runs per page and uses every core you give it by default. A 2 GB / 2 vCPU box handles a household's paperwork; a large backlog import on the same box will simply take longer.
If you are tight on memory, the upstream docs suggest a few knobs for
less powerful machines, all set in docker-compose.env:
PAPERLESS_WEBSERVER_WORKERS=1saves some memory.PAPERLESS_TASK_WORKERSandPAPERLESS_THREADS_PER_WORKERlimit how many documents and pages are processed in parallel.PAPERLESS_OCR_CLEAN=nonespeeds up OCR and uses less memory, at the cost of slightly worse results.
Disk grows with your archive: each document is stored as the original and an archived PDF/A copy, plus a thumbnail. Start with 40 GB and watch it.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, with a
ufw firewall. If not, work through
Docker & Compose on Ubuntu first.
Only SSH and the reverse proxy's ports should be open. Paperless's own port 8000 stays on loopback:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Paid link — we earn a commission if you shop through it.
Install Paperless-ngx (Docker Compose + PostgreSQL)
Paperless ships an interactive installer script, and it works well at a
terminal. This guide uses the other documented route — the Compose templates
from the repository's docker/compose directory — because every setting ends up
in a file you can read, back up and version.
Upstream recommends PostgreSQL for new installations. Download the Postgres
template as docker-compose.yml, plus its two env files:
mkdir -p ~/paperless && 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 docker-compose.env "$BASE/docker-compose.env"
curl -fsSL -o .env "$BASE/.env"
mkdir -p consume export
The template publishes port 8000 on every interface. Rebind it to loopback so the reverse proxy is the only way in:
cd ~/paperless
sed -i 's|- "8000:8000"|- "127.0.0.1:8000:8000"|' docker-compose.yml
grep -n '8000' docker-compose.yml
Now the settings. The template's PAPERLESS_SECRET_KEY is the literal
change-me, which you must replace. Set the container's user to your own UID/GID
so you can drop files into ./consume and read ./export without sudo, and
let Paperless create the first superuser from two environment variables instead
of an interactive prompt:
cd ~/paperless
sed -i "s|^PAPERLESS_SECRET_KEY=.*|PAPERLESS_SECRET_KEY=$(openssl rand -hex 32)|" docker-compose.env
ADMIN_PASSWORD="$(openssl rand -hex 16)"
cat >> docker-compose.env <<EOF
USERMAP_UID=$(id -u)
USERMAP_GID=$(id -g)
PAPERLESS_TIME_ZONE=UTC
PAPERLESS_OCR_LANGUAGE=eng
PAPERLESS_ADMIN_USER=admin
PAPERLESS_ADMIN_PASSWORD=$ADMIN_PASSWORD
EOF
echo "Paperless admin password: $ADMIN_PASSWORD"
Save that password in your password manager. PAPERLESS_ADMIN_USER creates the
superuser at start if it doesn't exist yet; once you have logged in, you can
remove both lines and change the password in the web UI.
PAPERLESS_OCR_LANGUAGE is the language most of your documents are written in.
The image ships English, German, Italian, Spanish and French; others are added
with PAPERLESS_OCR_LANGUAGES (for example tur ces).
Pull and start the stack:
cd ~/paperless
docker compose pull
docker compose up -d
docker compose ps
The first start runs the database migrations, which takes a minute or two. Wait until the login page answers:
for i in $(seq 1 60); do
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/accounts/login/)
[ "$code" = "200" ] && echo "Paperless is up" && break
sleep 5
done
[ "$code" = "200" ]
The database password in the template is paperless. The db service
publishes no port, so it is reachable only from the other containers on the
Compose network; change it (in both POSTGRES_PASSWORD and a
PAPERLESS_DBPASS entry) if you want defence in depth, before the first start.
HTTPS + domain
Point an A record such as paperless.example.com at the server, then tell
Paperless its public address. PAPERLESS_URL sets the allowed hosts, CORS hosts
and CSRF trusted origins in one go; without it, logins through the proxy fail
the CSRF check:
cd ~/paperless
echo "PAPERLESS_URL=https://paperless.example.com" >> docker-compose.env
docker compose up -d
Then terminate TLS in front of 127.0.0.1:8000, following
Automatic HTTPS with Caddy:
paperless.example.com {
request_body {
max_size 100MB
}
reverse_proxy 127.0.0.1:8000
}
The request_body limit is optional; raise it if you upload large scans through
the browser. If Caddy runs as a container, 127.0.0.1 is the proxy's own
loopback: put both in one Compose project and use
reverse_proxy webserver:8000 instead.
Getting documents in
There are three routes, and you will probably use all of them:
- Upload in the browser — drag files onto the dashboard.
- The consume folder — anything written to
~/paperless/consumeis picked up, processed and removed. Point a scanner's SMB/FTP target or a sync tool at it. This is why theUSERMAP_UIDsetting above matters. - Mail — Paperless can poll IMAP mailboxes and consume attachments, set up under Mail in the web UI.
Each new document goes through OCR, gets a date, and has tags, correspondent and type suggested from what you taught it earlier. Expect the first week to involve correcting a lot of those.
Securing it
- Keep port 8000 on loopback. The sed step above does that;
ss -ltnpshould show127.0.0.1:8000, not0.0.0.0:8000. - Don't use the superuser day to day. Upstream's own advice is to create a separate, normal user for daily use, or downgrade the superuser after setup, because a superuser has access to every document.
- Remove
PAPERLESS_ADMIN_PASSWORDfrom the env file once you have logged in and changed the password. - Enable two-factor authentication for your account in the profile settings.
Backups
Paperless has a built-in document exporter that writes every document,
thumbnail, the metadata and the database contents into a folder — the upstream
recommendation for backups. The Compose template already mounts ./export:
cd ~/paperless
docker compose exec -T webserver document_exporter ../export --no-progress-bar
ls -la export
-T avoids "the input device is not a TTY" errors when this runs from cron. The
exporter updates an existing export in place, so it pairs well with rsync to
another machine for incremental copies. Add -z to write a single zip file
instead.
Two limits to know, both from the upstream docs: an export can only be imported
into the same Paperless version that produced it, and it does not
include API tokens. Record the version with each backup, and copy
docker-compose.yml, docker-compose.env and .env alongside the export:
cd ~/paperless
tar czf paperless-config-$(date +%F).tar.gz docker-compose.yml docker-compose.env .env
Copy both off the server, and restore into a scratch instance once so you know the procedure works.
Upgrades
Make a backup first, check that nothing is being consumed, then:
cd ~/paperless
docker compose pull
docker compose up -d
The container applies database migrations on start. Read the release notes
before a major version: the v3 upgrade, for example, clears the existing task
history. Pin a specific tag (ghcr.io/paperless-ngx/paperless-ngx:3.2.1) instead
of latest if you want to choose when that happens.
Troubleshooting
Login through the domain fails with a CSRF error. PAPERLESS_URL is missing
or doesn't match the exact https:// origin you use.
Files in consume are never picked up. Check ownership: the container
writes as USERMAP_UID, and a folder created by root blocks it. Also check
docker compose logs webserver for the consumer's messages.
OCR is slow and the box swaps. Lower PAPERLESS_TASK_WORKERS and
PAPERLESS_THREADS_PER_WORKER, or add RAM. Large imports are CPU-bound; let them
run overnight.
Text isn't found in a non-English document. Set PAPERLESS_OCR_LANGUAGE
(and PAPERLESS_OCR_LANGUAGES if the language isn't bundled), then reprocess the
document.
Verification + next steps
You're done when you can load https://paperless.example.com over a valid
certificate, log in, upload a scanned PDF and find it by searching for a word
inside it, and produce an export you have copied off the box.
From there, set up tags and a few matching rules, point your scanner at the consume folder, and add mail polling. For the PDFs that need editing before they go into the archive — merging, splitting, redacting — pair it with Stirling-PDF. For hosts, see Best VPS for Self-Hosting.