Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

How to Self-Host Paperless-ngx with Docker on Ubuntu 24.04

Published

Self Hosted
How to Self-Host Paperless-ngx with Docker

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

Account services are unavailable until the production backend is configured.