Skip to content

How to Deploy Paperless-ngx on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-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.

Before you start
  • 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)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

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=1 saves some memory.
  • PAPERLESS_TASK_WORKERS and PAPERLESS_THREADS_PER_WORKER limit how many documents and pages are processed in parallel.
  • PAPERLESS_OCR_CLEAN=none speeds 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
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

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/consume is picked up, processed and removed. Point a scanner's SMB/FTP target or a sync tool at it. This is why the USERMAP_UID setting 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 -ltnp should show 127.0.0.1:8000, not 0.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_PASSWORD from 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.

Next steps

How to self-host Paperless-ngx →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.