Paperless-ngx
A document management system that digitizes and organizes physical and digital documents.
Paperless-ngx is an open-source document management system that scans, OCRs, tags, and indexes your documents for full-text search. It automatically classifies documents using machine learning and organizes them into a searchable archive. Self-hosting eliminates subscription costs and keeps sensitive documents off third-party servers.
Alternatives considered
Cloud Hosted
| Tool | Open Source | Free Tier | Monthly Cost |
|---|---|---|---|
| DocuWare | No | No | Enterprise |
| Google Drive | No | Limited | From $2.99/mo |
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| Papermerge | Yes | Yes | Simpler DMS with OCR |
Installation
Architecture
- Deployment: Single
paperlessdeployment in thepaperlessnamespace - Images:
paperlessngx/paperless-ngx,valkey/valkey, both digest-pinned via Renovate - Cache/Queue: Valkey for Celery task queue
- Database: CNPG PostgreSQL cluster with Longhorn-encrypted PVCs
- Storage: Two Longhorn PVCs —
paperless-data-encryptedandpaperless-media-encrypted - Networking: HTTPRoute via internal gateway
Security
- Runs as
runAsUser: 999,runAsNonRoot: true,allowPrivilegeEscalation: false, capabilities dropped - All PVCs encrypted at rest via SOPS-managed keys
Updates
Managed by Renovate. Both images are digest-pinned.
Data Management
- Database: CNPG PostgreSQL cluster (Longhorn-encrypted PVCs)
- PVCs:
paperless-data-encrypted(documents/config) andpaperless-media-encrypted(scanned files), both Longhorn-encrypted and annotatedk8up.io/backup: "true" - Backups: k8up
Schedules to Hetzner and Backblaze via restic, covering the data and media PVCs and the database dump from thebackups-cnpgPreBackupPod.
User Management
No OIDC configured. Users managed through the Paperless-ngx admin UI. Registration and superuser settings configurable via env vars.
Configuration Management
- Database credentials and secret key from SOPS-encrypted secret
- Database host/name/user injected from CNPG-generated secret
- Longhorn encryption keys from SOPS-encrypted secrets
Usage
Upload or scan documents via the web UI, email ingestion, or the mobile app. Paperless-ngx OCRs each document and enables full-text search. Use tags, document types, and correspondents to organize your archive. The consumption folder monitors for new files to process automatically.
Cluster-specific deviations from the above live in the per-cluster README — see k8s/apps/paperless/talos/README.md.
Cluster Deployment
Paperless-ngx — Talos cluster
- Image:
paperlessngx/paperless-ngx:3.3.2@sha256:d232cba37cc0bab0d6c6581ecd320383b7a6f49f33e0fd16adda4d009eb600dd - Image:
valkey/valkey:9.1.2-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app.kubernetes.io/name: paperless
name: paperless
namespace: paperless
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: paperless
strategy:
rollingUpdate: null
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: paperless
spec:
automountServiceAccountToken: false
containers:
- env:
- name: PAPERLESS_DBENGINE
value: postgresql
- name: PAPERLESS_DBHOST
valueFrom:
secretKeyRef:
key: host
name: cnpg-app
- name: PAPERLESS_DBNAME
valueFrom:
secretKeyRef:
key: dbname
name: cnpg-app
- name: PAPERLESS_DBUSER
valueFrom:
secretKeyRef:
key: user
name: cnpg-app
- name: PAPERLESS_DBPASS
valueFrom:
secretKeyRef:
key: password
name: cnpg-app
- name: PAPERLESS_EMAIL_HOST
valueFrom:
secretKeyRef:
key: host
name: allinkl-smtp-credentials
- name: PAPERLESS_EMAIL_PORT
valueFrom:
secretKeyRef:
key: port
name: allinkl-smtp-credentials
- name: PAPERLESS_EMAIL_USE_TLS
value: 'true'
- name: PAPERLESS_EMAIL_HOST_USER
valueFrom:
secretKeyRef:
key: username
name: allinkl-smtp-credentials
- name: PAPERLESS_EMAIL_HOST_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: allinkl-smtp-credentials
- name: PAPERLESS_EMAIL_FROM
valueFrom:
secretKeyRef:
key: from
name: allinkl-smtp-credentials
- name: TZ
value: America/Anchorage
- name: PAPERLESS_PORT
value: '8000'
- name: PAPERLESS_REDIS
value: redis://valkey:6379
envFrom:
- secretRef:
name: paperless
image: paperlessngx/paperless-ngx:3.3.2@sha256:d232cba37cc0bab0d6c6581ecd320383b7a6f49f33e0fd16adda4d009eb600dd
livenessProbe:
failureThreshold: 5
httpGet:
path: /
port: 8000
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 5
name: paperless
ports:
- containerPort: 8000
name: web
protocol: TCP
readinessProbe:
failureThreshold: 5
httpGet:
path: /
port: 8000
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 2
resources:
limits:
memory: 2Gi
requests:
cpu: 150m
memory: 2Gi
startupProbe:
failureThreshold: 30
httpGet:
path: /
port: 8000
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- mountPath: /usr/src/paperless/data
name: paperless-data
- mountPath: /usr/src/paperless/media
name: paperless-media
- mountPath: /data
name: storage-volume
priorityClassName: tier-1
terminationGracePeriodSeconds: 60
volumes:
- name: paperless-data
persistentVolumeClaim:
claimName: paperless-data-encrypted
- name: paperless-media
persistentVolumeClaim:
claimName: paperless-media-encrypted
- emptyDir: {}
name: storage-volume
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
name: valkey
namespace: paperless
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: valkey
serviceName: valkey
template:
metadata:
labels:
app.kubernetes.io/name: valkey
spec:
containers:
- args:
- valkey-server
image: valkey/valkey:9.1.2-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 10
tcpSocket:
port: 6379
name: valkey
ports:
- containerPort: 6379
name: client
readinessProbe:
initialDelaySeconds: 3
periodSeconds: 5
tcpSocket:
port: 6379
resources:
limits:
memory: 512Mi
requests:
cpu: 50m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
volumeMounts:
- mountPath: /conf
name: conf
- mountPath: /data
name: data
priorityClassName: tier-1
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
volumes:
- emptyDir: {}
name: conf
- emptyDir: {}
name: data