Skip to main content

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

ToolOpen SourceFree TierMonthly Cost
DocuWareNoNoEnterprise
Google DriveNoLimitedFrom $2.99/mo

Self Hosted

ToolOpen SourceFull FeaturesNotes
PapermergeYesYesSimpler DMS with OCR

Installation​

Architecture​

  • Deployment: Single paperless deployment in the paperless namespace
  • 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-encrypted and paperless-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) and paperless-media-encrypted (scanned files), both Longhorn-encrypted and annotated k8up.io/backup: "true"
  • Backups: k8up Schedules to Hetzner and Backblaze via restic, covering the data and media PVCs and the database dump from the backups-cnpg PreBackupPod.

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

Kubernetes Metadata
  • 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