GICT knowledge base
How to Self-Host Paperless-ngx with Docker on Ubuntu 24.04
Published
A scanned invoice is easy to save and hard to find again. Paperless-ngx reads text from scanned documents, stores the originals, and lets you search their contents in a browser.
Before you start
Use a fresh Ubuntu 24.04 VPS with sudo access. Plan for 2 CPU cores, 3 GB RAM, and enough disk space for the container images, originals, and searchable copies. This is the configuration used for the test, not a capacity guarantee for large document batches. A shared-hosting account without Docker access is not suitable.
The setup handles PDFs and common image formats with English OCR. Office documents need additional services and are outside this guide. SSH-tunnel access suits a private archive; it is not a public URL you can share with colleagues.
Install Docker and Compose
Check existing listeners and Docker packages first:
$ sudo ss -lntp
$ dpkg -l docker.io docker-ce docker-compose-v2 docker-compose-plugin
Missing packages in the second command are normal on a fresh VPS. If Docker is already installed from Docker’s repository, keep that installation and its Compose plugin rather than mixing it with Ubuntu’s packages.
On a fresh Ubuntu installation, run:
$ sudo apt-get update
$ sudo apt-get install -y docker.io docker-compose-v2 openssl curl
$ sudo systemctl enable --now docker
$ sudo docker version --format '{{.Server.Version}}'
$ sudo docker compose version
Use Docker Engine 28 or later and Compose v2. Earlier Docker releases have a localhost port-publishing limitation that can allow access from another machine on the same network. Stop here and update Docker through its supported installation method if the server reports an older version.
Create the project files
Create a private directory as your normal sudo-capable user:
$ mkdir -p ~/paperless
$ chmod 700 ~/paperless
$ cd ~/paperless
$ mkdir -p consume export
$ id -u
$ id -g
Note the two numeric IDs. They tell Paperless which user should own files in the shared directories.
Generate two different random values, one for the database password and one for the application secret:
$ openssl rand -hex 32
$ openssl rand -hex 32
Keep these values private. Create ~/paperless/.env with the following contents, replacing both REPLACE_ values and the user IDs before continuing:
PAPERLESS_VERSION=3.1.3
POSTGRES_PASSWORD=REPLACE_WITH_FIRST_RANDOM_VALUE
PAPERLESS_SECRET_KEY=REPLACE_WITH_SECOND_RANDOM_VALUE
USERMAP_UID=1000
USERMAP_GID=1000
PAPERLESS_TIME_ZONE=Etc/UTC
Use an IANA timezone such as Europe/London if UTC is not appropriate. The application secret is required by Paperless-ngx; it is not your login password.
Protect the file:
$ chmod 600 .env
Create ~/paperless/compose.yaml:
name: paperless
services:
broker:
image: docker.io/valkey/valkey:9-alpine
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
restart: unless-stopped
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:${PAPERLESS_VERSION:?Set PAPERLESS_VERSION}
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "127.0.0.1:8000:8000"
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBHOST: db
PAPERLESS_DBNAME: paperless
PAPERLESS_DBUSER: paperless
PAPERLESS_DBPASS: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
PAPERLESS_SECRET_KEY: ${PAPERLESS_SECRET_KEY:?Set PAPERLESS_SECRET_KEY}
PAPERLESS_URL: http://localhost:8000
PAPERLESS_TIME_ZONE: ${PAPERLESS_TIME_ZONE}
PAPERLESS_OCR_LANGUAGE: eng
PAPERLESS_TASK_WORKERS: 1
PAPERLESS_THREADS_PER_WORKER: 2
USERMAP_UID: ${USERMAP_UID}
USERMAP_GID: ${USERMAP_GID}
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./consume:/usr/src/paperless/consume
- ./export:/usr/src/paperless/export
volumes:
data:
media:
pgdata:
redisdata:
This follows the project’s PostgreSQL Compose template, with a pinned Paperless release, private application listener, and explicit credentials. PostgreSQL 18 uses /var/lib/postgresql for its volume mount; do not substitute the older /var/lib/postgresql/data path from an unrelated guide.
PostgreSQL and Valkey have no published host ports. The data, media, and database volumes hold the persistent application state. Do not run docker compose down -v on an archive you want to keep: that removes the named volumes.
Start Paperless-ngx
From ~/paperless, validate without printing the resolved passwords:
$ sudo docker compose config --quiet
$ sudo docker compose pull
$ sudo docker compose up -d
$ sudo docker compose ps
The first start initializes PostgreSQL and runs database migrations. Wait for the webserver to become healthy. If it exits or remains unhealthy, inspect the application log:
$ sudo docker compose logs --tail=100 webserver
Do not paste unreviewed logs into a public support request. Once the application is ready, check its local response:
$ curl -I http://127.0.0.1:8000/
A signed-out request normally redirects to the login page. Inspect the actual status and Location header. This confirms a responding listener, not successful document processing.
Open the private web interface
On your own computer, open another terminal and start a tunnel. Replace ubuntu and VPS_IP with your SSH username and server address:
$ ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:8000:127.0.0.1:8000 ubuntu@VPS_IP
Use your normal SSH key options and add -p YOUR_SSH_PORT if needed. A successful tunnel remains running without a shell prompt. Keep that terminal open while using Paperless.
Open http://localhost:8000 in the browser on that computer. Use this hostname consistently because it matches PAPERLESS_URL. Although the browser URL is HTTP, the connection between your computer and the VPS travels inside SSH. Do not change the Compose binding to 8000:8000 to make the page reachable remotely.
The initial page prompts you to create a superuser. Choose a unique username and strong password, then sign in. This account has access to all documents. There is no need to publish the application port to complete setup.
Upload a scan and verify OCR
Start with a single clear English scan containing a phrase you can recognize. Use the web interface to upload the PDF or image, then wait for processing to finish. An uploaded file is not necessarily a processed document.
Open the resulting document. Check the preview, then select the Content tab to read the recognized text. Enter distinctive words from the page, not its filename or title, in the search box and press Enter. Use plain words without quotation marks in this default search. The document should appear in the results.
To retrieve the uploaded file, open the document, click the arrow beside Download, and choose Download original. Open the downloaded file to confirm it is usable.
Use a non-sensitive image-only test PDF containing a distinctive phrase. Wait for processing to complete, search for that phrase, then download the original and compare its checksum with the uploaded file. Check OCR accuracy separately from file integrity; processing time varies with the input and available resources.
Use non-sensitive sample documents for this first check. OCR can misread faint print, handwriting, or rotated pages; a successful import is not a guarantee that every word is correct.
If the first import does not work
| Symptom | What to check |
|---|---|
| The browser cannot connect | Keep the SSH tunnel open; confirm the webserver is healthy. The VPS public address is deliberately not the browser endpoint. |
| SSH says the local address is already in use | Another local process or tunnel owns port 8000. Close that process if appropriate, then retry the tunnel. |
| Database authentication fails | Both services must use the same database password. Changing .env after PostgreSQL has initialized does not change the existing database user’s password. |
A permissions error mentions consume or export |
Check that USERMAP_UID and USERMAP_GID match your server user, and that this user owns those directories. |
| The document imports but its text is wrong | Check scan quality and OCR language. eng is English; other languages may require additional installed language packs. |
For a failed import, inspect the webserver log using the command above. Correct the reported cause before submitting another copy of the same file.
Once upload, content search, and original download work, the installation is complete. Your documents remain on the VPS, and browser access stays behind SSH.
Related guides
How to Self-Host DocuSeal for Document Signing
Self-host DocuSeal with private administrator setup, PostgreSQL and HTTPS. Test signing, understand email data flows and create consistent backups.
Self HostedHow to Self-Host Docmost with Docker on Ubuntu 24.04
Install Docmost with Docker, PostgreSQL and Redis. Set up a private workspace and verify editing, live updates and attachment retrieval.
Self HostedHow to Remove Image Backgrounds Locally with rembg
Remove image backgrounds locally with rembg. Preserve originals, check PNG transparency and inspect cutout quality on both light and dark backgrounds.
Self HostedHow to Translate Text Offline with Argos Translate
Translate plain text locally with Argos Translate. Install a language pair, prepare caches, verify offline operation and review translation quality.
