Skip to main content

Paperless-ngx Server Documentation

Overview

Paperless-ngx is a document management system running on the paperless server. It is deployed using Podman with systemd Quadlet units (rootful, managed by root via systemd). The stack consists of three containers: the Paperless-ngx web application, a PostgreSQL database, and a Redis cache — all communicating over a private internal Podman network.

URL: https://paperless.home.helix9.org Authentication: Authentik SSO (OpenID Connect) OCR Languages: German (primary), English


Architecture

┌─────────────────────────────────┐
│ paperless server │
│ │
Internet/LAN │ ┌────────────────────────────┐ │
──────────────:8000──►│ │ paperless-webserver │ │
│ │ (paperless-ngx:latest) │ │
│ └──────────┬─────────────────┘ │
│ │ paperless-internal │
│ ┌──────────┴──────────────────┐ │
│ │ paperless-internal.network │ │
│ └──────────┬──────────┬───────┘ │
│ │ │ │
│ ┌──────────▼──┐ ┌───▼─────────┐ │
│ │ paperless-db│ │paperless- │ │
│ │ (postgres:18│ │redis (redis:│ │
│ │ │ │8) │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Storage: /HDD/paperless/ │
└─────────────────────────────────┘

Infrastructure

ComponentDetails
OS disk10 GB SSD (btrfs, ~23% used)
Data disk15 TB HDD mounted at /HDD (~24% used / ~3.5 TB)
Container runtimePodman (rootful, systemd Quadlet)
Container auto-updateEnabled via AutoUpdate=registry

Systemd Quadlet Units

The containers are defined in /etc/containers/systemd/ as Quadlet .container and .network files. Systemd generates service units from these at boot.

Services

ServiceDescriptionStatus
paperless-webserver.serviceMain applicationRunning
paperless-db.servicePostgreSQL 18Running
paperless-redis.serviceRedis 8 cacheRunning
paperless-internal-network.serviceInternal Podman networkActive

Manage with:

# Status
systemctl status paperless-webserver paperless-db paperless-redis

# Restart individual service
systemctl restart paperless-webserver

# Restart full stack
systemctl restart paperless-db paperless-redis paperless-webserver

# Logs
journalctl -u paperless-webserver -f
journalctl -u paperless-db -f

Quadlet File Locations

/etc/containers/systemd/
├── paperless-internal.network # Private network definition
├── paperless-webserver.container # Main app
├── paperless-db.container # PostgreSQL
└── paperless-redis.container # Redis

paperless-webserver.container

[Unit]
Description=Paperless-ngx
After=network-online.target paperless-redis.service paperless-db.service
Wants=network-online.target
Requires=paperless-redis.service paperless-db.service

[Container]
Image=ghcr.io/paperless-ngx/paperless-ngx:latest
AutoUpdate=registry
ContainerName=paperless-ngx
Volume=/HDD/paperless/data:/usr/src/paperless/data
Volume=/HDD/paperless/media:/usr/src/paperless/media
Volume=/HDD/paperless/export:/usr/src/paperless/export
Volume=/HDD/paperless/consume:/usr/src/paperless/consume
EnvironmentFile=/srv/podman/paperless/docker-compose.env
Environment=PAPERLESS_REDIS=redis://paperless-redis:6379
Environment=PAPERLESS_DBHOST=paperless-db
Network=paperless-internal.network
PublishPort=8000:8000

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

paperless-db.container

[Unit]
Description=Paperless PostgreSQL
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/postgres:18
AutoUpdate=registry
ContainerName=paperless-db
Volume=paperless_pgdata:/var/lib/postgresql
EnvironmentFile=/srv/podman/paperless/docker-compose.env
Environment=POSTGRES_DB=paperless
Environment=POSTGRES_USER=paperless
Environment=POSTGRES_PASSWORD=<see /srv/podman/paperless/docker-compose.env>
Network=paperless-internal.network

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

paperless-redis.container

[Unit]
Description=Paperless Redis
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/redis:8
AutoUpdate=registry
ContainerName=paperless-redis
Volume=paperless_redisdata:/data
Network=paperless-internal.network

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

Configuration

The main configuration file is at /srv/podman/paperless/docker-compose.env.

Key Settings

VariableValue
PAPERLESS_URLhttps://paperless.home.helix9.org
PAPERLESS_TIME_ZONEEurope/Berlin
PAPERLESS_OCR_LANGUAGEdeu (German primary)
PAPERLESS_OCR_LANGUAGESdeu en
PAPERLESS_FILENAME_FORMAT{created_year}-{created_month}-{created_day} - ASN{asn:05} - {title}

Barcode / ASN Scanner Settings

Paperless is configured to automatically read ASN barcodes from scanned documents:

VariableValue
PAPERLESS_CONSUMER_ENABLE_BARCODEStrue
PAPERLESS_CONSUMER_ENABLE_ASN_BARCODEtrue
PAPERLESS_CONSUMER_ASN_BARCODE_PREFIXASN
PAPERLESS_CONSUMER_BARCODE_SCANNERZXING
PAPERLESS_CONSUMER_BARCODE_UPSCALE1.5

When a document is dropped into the consume folder, Paperless scans for a QR code with the ASN prefix and assigns the ASN automatically.

SSO (Authentik)

Login is handled via Authentik (OpenID Connect). The Authentik application is at: https://auth.home.helix9.org/application/o/paperless/

Credentials and OIDC client configuration are stored in /srv/podman/paperless/docker-compose.env.


Storage Layout

All data lives under /HDD/paperless/ on the large HDD:

/HDD/paperless/
├── consume/ # Drop documents here for automatic import
├── data/ # Internal app data (index, ML model, celery schedule)
├── export/ # Manual export outputs
└── media/
└── documents/ # Stored document files

Podman named volumes (for database/redis):

paperless_pgdata # PostgreSQL data
paperless_redisdata # Redis persistence

Consuming Documents

Drop PDF/image files into /HDD/paperless/consume/ — Paperless automatically picks them up, runs OCR, detects ASN barcodes, and files the document.

From the paperless server:

cp document.pdf /HDD/paperless/consume/

Container Auto-Updates

All three containers have AutoUpdate=registry set. Podman's auto-update mechanism checks the registry for newer images and updates automatically.

Manual update:

podman auto-update

Check container status:

podman ps # run as root

ASN Label Printer

An ASN (Archive Serial Number) label printer is set up on the paperless server to print physical labels that get attached to physical documents before scanning. The label contains a QR code and human-readable ASN number.

Hardware: Brother QL-570 USB thermal label printer Labels: 62×29mm die-cut labels Location: /home/marko/asn-label-printer/

How It Works

  1. Run print-asn on the paperless server to print the next available label
  2. Attach the label to a physical document
  3. Scan the document into Paperless (via consume folder or scanner)
  4. Paperless reads the ASN QR code and assigns the matching ASN to the document
  5. The file is stored as: YYYY-MM-DD - ASNxxxxx - <title>

Files

/home/marko/asn-label-printer/
├── asn_printer.py # Main script
├── config.json # Counter and printer settings
├── requirements.txt # Python dependencies
├── setup.sh # Initial setup script
├── verify.sh # Hardware/software verification script
├── preview.png # Last generated preview image
└── tests/
└── test_label.py # Test suite

Configuration (config.json)

{
"asn_counter": 101,
"printer_model": "QL-570",
"printer_backend": "pyusb",
"label_size": "62x29",
"asn_prefix": "ASN",
"asn_digits": 5
}

The asn_counter auto-increments with each successful print. ASNs are formatted as ASN00001 through ASN99999.

Usage

The shell is configured to activate the venv and cd into the directory on login. A print-asn alias is available:

# Print next label (auto-increments counter)
print-asn

# Check current counter / next ASN
print-asn --status

# Print a specific ASN (does NOT increment counter)
print-asn -n 42

# Print a range of labels (does NOT increment counter)
print-asn --range 1 50

# Generate a preview PNG without printing
print-asn --preview

# Reset counter to a specific value
print-asn --reset 100

# Use from outside (e.g., via SSH)
ssh paperless "cd ~/asn-label-printer && source venv/bin/activate && ./asn_printer.py"

Label Layout

┌──────────────────────────────┐
│ │
│ ┌─────────┐ │
│ │ █ █ █ │ │
│ │ QR Code│ │
│ │ █ █ █ │ │
│ └─────────┘ │
│ │
│ ASN00101 │
│ │
└──────────────────────────────┘
  • QR code: 180×180px, error correction level M, contains plain ASNxxxxx text
  • Text: 56pt bold, centered below QR code
  • Total label: 696×271px (62mm×29mm at 300 DPI), 1-bit B&W

Python Dependencies

brother-ql==0.9.4
qrcode[pil]==7.4.2
Pillow>=11.0.0

USB Permissions

A udev rule grants non-root access to the Brother printer:

/etc/udev/rules.d/99-brother-ql.rules:
SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2028", MODE="0666"

Verification

Run ~/asn-label-printer/verify.sh to check USB connection, udev rules, Python environment, and dependencies:

cd ~/asn-label-printer && bash verify.sh

Troubleshooting

Paperless containers not starting

journalctl -u paperless-webserver --since "1 hour ago"
journalctl -u paperless-db --since "1 hour ago"
systemctl status paperless-webserver paperless-db paperless-redis

Check disk space

df -h /HDD
df -h /

Access container shell

# Run as root on paperless server
podman exec -it paperless-ngx bash
podman exec -it paperless-db psql -U paperless

ASN printer not found

cd ~/asn-label-printer
lsusb | grep Brother # should show 04f9:2028
bash verify.sh # full verification
source venv/bin/activate
./asn_printer.py --status # check counter

If pyusb backend fails, change printer_backend to linux_kernel in config.json:

{
"printer_backend": "linux_kernel"
}

Counter out of sync with Paperless

If the printer counter and Paperless ASN index get out of sync:

# Check next ASN in Paperless UI: Settings → ASN
# Then reset counter to match
cd ~/asn-label-printer
source venv/bin/activate
./asn_printer.py --reset <next_asn_number>