Paperless-ngx — Setup
Self-hosted document management system with OCR, full-text search, and auto-classification. Runs as a Docker Compose stack (Paperless + PostgreSQL + Redis) on a dedicated Debian LXC (124). OIDC via PocketID. Document files stored on urahara, DB and search index on heavy-pool SSD.
Infrastructure
| Host | LXC ID | Internal | External | CPU | RAM | Disk |
|---|---|---|---|---|---|---|
| Debian LXC | 124 | 192.168.1.124:8000 | https://paperless.eva-00.network | 2 cores | 2 GiB | 16 GiB |
Storage Layout
| Path | Pool | Type | Purpose |
|---|---|---|---|
/opt/paperless/database/ |
heavy-pool (SSD) | PostgreSQL data | Fast random I/O for queries |
/opt/paperless/data/ |
heavy-pool (SSD) | Search index + classification models | Fast reads for snappy search |
/opt/paperless/redis/ |
heavy-pool (SSD) | Redis persistence | Task queue state |
/mnt/paperless/media/ |
urahara (HDD) | Original + archived documents | Write-once bulk storage |
/opt/paperless/consume/ |
heavy-pool (SSD) | Ingestion directory | Temporary — files move to media after processing |
/opt/paperless/export/ |
heavy-pool (SSD) | Document export directory | On-demand exports |
Bind mount: /mnt/pve/urahara/paperless/media → /mnt/paperless/media
Observability
Logs
Container logs are collected via Grafana Alloy Docker discovery and shipped to Loki.
| Query | Purpose |
|---|---|
{container="paperless"} |
Paperless-ngx app logs |
{container="paperless-postgres"} |
PostgreSQL logs |
{container="paperless-redis"} |
Redis logs |
{container=~"paperless.*"} \|= "error" |
All errors |
{container="paperless"} \|= "ocr" |
OCR processing |
Access: Grafana → Explore → Loki → Enter query
Metrics
Paperless-ngx does not export Prometheus metrics by default. Use Loki logs to diagnose issues.
IaC
| Artifact | Path |
|---|---|
| Playbook | ansible/playbooks/paperless.yml |
| Workflow | .forgejo/workflows/paperless.yml |
| Docker Compose | services/paperless/docker-compose.yml |
| Caddy entry | services/caddy/Caddyfile → paperless.eva-00.network |
| Glance entry | services/glance/glance.yml → Knowledge section |
| Gatus entry | services/gatus/config.yaml → Paperless-ngx endpoint |
The playbook manages the full lifecycle:
- Provisions LXC 124 on Proxmox (if not exists)
- Installs Docker
- Fetches secrets from Vault
- Creates directories (
/opt/paperless/{data,database,redis,export,consume}) - Deploys Docker Compose stack (Paperless + PostgreSQL + Redis)
- Auto-creates superuser via
PAPERLESS_ADMIN_USERenv var (first run only) - SSO via PocketID OIDC — internal login disabled
No first-run UI wizard. Everything is configured via environment variables.
Auth
| Component | Details |
|---|---|
| OIDC Provider | PocketID (auth.eva-00.network) |
| Method | Native django-allauth (no oauth2-proxy sidecar) |
| Callback URL | https://paperless.eva-00.network/accounts/oidc/pocketid/login/callback/ |
| Auth mode | OIDC-only (PAPERLESS_DISABLE_REGULAR_LOGIN=true) |
| Auto-redirect | Enabled (PAPERLESS_REDIRECT_LOGIN_TO_SSO=true) |
PocketID Setup
Create an OIDC client in PocketID:
| Field | Value |
|---|---|
| Name | paperless |
| Redirect URI | https://paperless.eva-00.network/accounts/oidc/pocketid/login/callback/ |
Store the client ID and secret in Vault at secret/paperless.
Secrets
Vault path: secret/data/paperless
| Key | Purpose |
|---|---|
secret_key |
Django secret key for session signing |
db_password |
PostgreSQL password |
admin_user |
Superuser username (used on first run) |
admin_password |
Superuser password (used on first run) |
pocketid_client_id |
OIDC client ID |
pocketid_client_secret |
OIDC client secret |
Seeding Vault
# Via vault-write workflow or direct API:
vault kv put secret/paperless \
secret_key="$(openssl rand -hex 32)" \
db_password="$(openssl rand -hex 16)" \
admin_user="claude" \
admin_password="$(openssl rand -hex 16)" \
pocketid_client_id="<from PocketID>" \
pocketid_client_secret="<from PocketID>"
Containers
| Container | Image | Purpose |
|---|---|---|
paperless |
ghcr.io/paperless-ngx/paperless-ngx:2.16 |
Web app + OCR + consumer |
paperless-postgres |
postgres:16-alpine |
Document metadata database |
paperless-redis |
redis:7-alpine |
Task queue broker |
Document Permissions & Sharing
Paperless-ngx has per-document permissions. Each document has an owner (the uploader) plus optional view/edit grants.
Sharing a document
Open a document → Permissions tab:
| Field | Purpose |
|---|---|
| Owner | Full control (only one user) |
| View users / groups | Read-only access |
| Edit users / groups | Can modify metadata, tags, etc. |
Bulk permissions
Select multiple documents from the list → use the bulk edit toolbar to set permissions on all at once.
Default permissions
Each user can configure defaults under Settings → Permissions:
| Setting | Effect |
|---|---|
| Default owner | Auto-set on new uploads (usually self) |
| Default view users/groups | Auto-grant view access on every upload |
| Default edit users/groups | Auto-grant edit access on every upload |
Example: set milton's default view users to include holo, so holo can always see milton's uploads without manual sharing.
Bot account (claude)
The claude bot user (used by Paperless-AI) has default_owner: None, so AI-created tags, correspondents, and document types are public — visible to all users. Documents keep their original owner.
AI Integration
Paperless-ngx can be extended with local AI via companion containers. See the Paperless AI Setup Guide for connecting to Ollama on LXC 107.