Deploy Paperless-ngx document management #328

Merged
eblume merged 8 commits from deploy-paperless into main 2026-04-08 17:54:13 -07:00
18 changed files with 489 additions and 0 deletions
Showing only changes of commit 041c47acfb - Show all commits

Deploy Paperless-ngx document management system

Add paperless-ngx (v2.20.13) as a new ArgoCD-managed service on indri
with Authentik OIDC SSO, PostgreSQL on blumeops-pg, Redis sidecar, and
NFS document storage on sifaka. Includes Dockerfile built from forge
mirror, full k8s manifests, Caddy route, 1Password secrets, and
reference documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Erich Blume 2026-04-08 16:33:34 -07:00

View file

@ -92,6 +92,9 @@ caddy_services:
- name: mealie
host: "meals.{{ caddy_domain }}"
backend: "https://meals.tail8d86e.ts.net"
- name: paperless
host: "paperless.{{ caddy_domain }}"
backend: "https://paperless.tail8d86e.ts.net"
- name: sifaka
host: "nas.{{ caddy_domain }}"
backend: "http://sifaka:5000"

View file

@ -0,0 +1,17 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: paperless
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/paperless
destination:
server: https://kubernetes.default.svc
namespace: paperless
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -65,6 +65,14 @@ spec:
createdb: true
passwordSecret:
name: blumeops-pg-authentik
# paperless user for Paperless-ngx document management
- name: paperless
login: true
connectionLimit: -1
ensure: present
inherit: true
passwordSecret:
name: blumeops-pg-paperless
# Resource limits for minikube environment
resources:

View file

@ -0,0 +1,28 @@
# ExternalSecret for Paperless database user password
#
# 1Password item: "Paperless (blumeops)" in blumeops vault
# Field: "postgresql-password"
#
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: blumeops-pg-paperless
namespace: databases
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: blumeops-pg-paperless
creationPolicy: Owner
template:
type: kubernetes.io/basic-auth
data:
username: paperless
password: "{{ .password }}"
data:
- secretKey: password
remoteRef:
key: Paperless (blumeops)
property: postgresql-password

View file

@ -14,3 +14,4 @@ resources:
- external-secret-immich-borgmatic.yaml
- external-secret-teslamate.yaml
- external-secret-authentik.yaml
- external-secret-paperless.yaml

View file

@ -0,0 +1,124 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless
namespace: paperless
spec:
replicas: 1
selector:
matchLabels:
app: paperless
template:
metadata:
labels:
app: paperless
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: paperless
image: registry.ops.eblu.me/blumeops/paperless:kustomized
ports:
- containerPort: 8000
name: http
env:
- name: PAPERLESS_URL
value: "https://paperless.ops.eblu.me"
- name: PAPERLESS_REDIS
value: "redis://localhost:6379"
- name: PAPERLESS_DBHOST
value: "pg.ops.eblu.me"
- name: PAPERLESS_DBPORT
value: "5432"
- name: PAPERLESS_DBNAME
value: "paperless"
- name: PAPERLESS_DBUSER
value: "paperless"
- name: PAPERLESS_DBPASS
valueFrom:
secretKeyRef:
name: paperless-secrets
key: db-password
- name: PAPERLESS_SECRET_KEY
valueFrom:
secretKeyRef:
name: paperless-secrets
key: secret-key
- name: PAPERLESS_TIME_ZONE
value: "America/Los_Angeles"
- name: PAPERLESS_OCR_LANGUAGE
value: "eng"
- name: PAPERLESS_TASK_WORKERS
value: "1"
# Admin account (created on first startup)
- name: PAPERLESS_ADMIN_USER
value: "erich"
- name: PAPERLESS_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: paperless-secrets
key: admin-password
- name: PAPERLESS_ADMIN_MAIL
value: "erich@eblu.me"
# OIDC via Authentik
# Full JSON blob pulled from 1Password (includes client secret)
- name: PAPERLESS_APPS
value: "allauth.socialaccount.providers.openid_connect"
- name: PAPERLESS_SOCIALACCOUNT_PROVIDERS
valueFrom:
secretKeyRef:
name: paperless-secrets
key: socialaccount-providers
- name: PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS
value: "true"
- name: PAPERLESS_SOCIAL_AUTO_SIGNUP
value: "true"
- name: PAPERLESS_REDIRECT_LOGIN_TO_SSO
value: "false"
volumeMounts:
- name: data
mountPath: /usr/src/paperless/data
- name: media
mountPath: /usr/src/paperless/media
- name: consume
mountPath: /usr/src/paperless/consume
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
- name: redis
image: docker.io/library/redis:kustomized
ports:
- containerPort: 6379
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "128Mi"
volumes:
- name: data
emptyDir: {}
- name: media
persistentVolumeClaim:
claimName: paperless-media
- name: consume
emptyDir: {}

View file

@ -0,0 +1,31 @@
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: paperless-secrets
namespace: paperless
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: paperless-secrets
creationPolicy: Owner
data:
- secretKey: db-password
remoteRef:
key: "Paperless (blumeops)"
property: postgresql-password
- secretKey: secret-key
remoteRef:
key: "Paperless (blumeops)"
property: secret-key
- secretKey: admin-password
remoteRef:
key: "Paperless (blumeops)"
property: admin-password
- secretKey: socialaccount-providers
remoteRef:
key: "Paperless (blumeops)"
property: socialaccount-providers

View file

@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless-tailscale
namespace: paperless
annotations:
tailscale.com/proxy-class: "default"
tailscale.com/proxy-group: "ingress"
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Paperless"
gethomepage.dev/group: "Home"
gethomepage.dev/icon: "paperless-ngx.png"
gethomepage.dev/description: "Document management"
gethomepage.dev/href: "https://paperless.ops.eblu.me"
gethomepage.dev/pod-selector: "app=paperless"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: paperless
port:
number: 8000
tls:
- hosts:
- paperless

View file

@ -0,0 +1,19 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: paperless
resources:
- deployment.yaml
- service.yaml
- pv-nfs.yaml
- pvc.yaml
- ingress-tailscale.yaml
- external-secret.yaml
images:
- name: registry.ops.eblu.me/blumeops/paperless
newTag: placeholder
- name: docker.io/library/redis
newName: registry.ops.eblu.me/blumeops/authentik-redis
newTag: v8.2.3-e04455c-nix

View file

@ -0,0 +1,22 @@
# NFS PersistentVolume for Paperless document library
# Requires: NFS share on sifaka at /volume1/paperless with NFS permissions for indri
#
# To create on Synology:
# 1. Control Panel > Shared Folder > Create
# 2. Name: paperless, Location: Volume 1
# 3. Control Panel > File Services > NFS > NFS Rules
# 4. Add rule for "paperless" share: Hostname=indri, Privilege=Read/Write, Squash=No mapping
apiVersion: v1
kind: PersistentVolume
metadata:
name: paperless-media-nfs-pv
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
nfs:
server: sifaka
path: /volume1/paperless

View file

@ -0,0 +1,15 @@
# PersistentVolumeClaim for Paperless document library
# Binds to the NFS PV for sifaka:/volume1/paperless
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-media
namespace: paperless
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: paperless-media-nfs-pv
resources:
requests:
storage: 500Gi

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: paperless
namespace: paperless
spec:
selector:
app: paperless
ports:
- name: http
port: 8000
targetPort: 8000
protocol: TCP

View file

@ -0,0 +1,128 @@
# Paperless-ngx — self-hosted document management
# Built from source via forge mirror of paperless-ngx/paperless-ngx
# Based on upstream Dockerfile (multi-stage: Node frontend + Python backend + s6-overlay)
ARG CONTAINER_APP_VERSION=v2.20.13
###############################################
# Frontend Build
###############################################
FROM node:20-slim AS frontend-builder
ARG CONTAINER_APP_VERSION
RUN apt-get update && apt-get install --no-install-recommends -y git ca-certificates && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/paperless-ngx.git /src
WORKDIR /src/src-ui
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN pnpm install --frozen-lockfile
RUN ./node_modules/.bin/ng build --configuration production
###############################################
# s6-overlay base
###############################################
FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-base
ARG S6_OVERLAY_VERSION=3.2.1.0
ARG TARGETARCH
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${TARGETARCH}.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-${TARGETARCH}.tar.xz \
&& rm -f /tmp/s6-overlay-*.tar.xz
ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
###############################################
# Main Application
###############################################
FROM s6-base AS production
ARG CONTAINER_APP_VERSION
# Runtime system dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \
# General
curl gosu tzdata gettext file libmagic1 media-types zlib1g \
# PDF / document processing
ghostscript gnupg qpdf poppler-utils imagemagick icc-profiles-free \
# OCR
tesseract-ocr tesseract-ocr-eng unpaper pngquant \
# Database client
postgresql-client \
# XML
libxml2 libxslt1.1 \
# Barcode
libzbar0 \
# Fonts
fonts-liberation \
# Build deps (purged after pip install)
build-essential pkg-config libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install jbig2enc from upstream prebuilt deb
ADD https://github.com/paperless-ngx/builder/releases/download/jbig2enc-0.29/jbig2enc_0.29-1_$(dpkg --print-architecture).deb /tmp/jbig2enc.deb
RUN dpkg -i /tmp/jbig2enc.deb && rm /tmp/jbig2enc.deb || true
WORKDIR /usr/src/paperless
# Clone source
RUN apt-get update && apt-get install --no-install-recommends -y git ca-certificates \
&& git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/paperless-ngx.git /tmp/paperless-src \
&& cp -r /tmp/paperless-src/src ./src \
&& cp -r /tmp/paperless-src/docker/rootfs / \
&& cp /tmp/paperless-src/docker/imagemagick-policy.xml /etc/ImageMagick-6/policy.xml || true \
&& cp /tmp/paperless-src/Pipfile* . 2>/dev/null || true \
&& cp /tmp/paperless-src/pyproject.toml /tmp/paperless-src/uv.lock . \
&& rm -rf /tmp/paperless-src \
&& apt-get purge -y git \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
# Copy frontend build
COPY --from=frontend-builder /src/src/documents/static/frontend/ /usr/src/paperless/src/documents/static/frontend/
# Install Python dependencies
ENV UV_LINK_MODE=copy
RUN uv export --frozen --no-dev --no-editable --no-emit-project \
--output-file /tmp/requirements.txt 2>/dev/null \
&& uv pip install --system -r /tmp/requirements.txt \
&& rm /tmp/requirements.txt \
|| uv pip install --system -e ".[postgres]"
# Download NLTK data
RUN python3 -c "import nltk; nltk.download('snowball_data', download_dir='/usr/share/nltk_data'); nltk.download('stopwords', download_dir='/usr/share/nltk_data'); nltk.download('punkt_tab', download_dir='/usr/share/nltk_data')" 2>/dev/null || true
# Create paperless user
RUN groupadd -g 1000 paperless \
&& useradd -u 1000 -g paperless -d /usr/src/paperless paperless
# Collect static files
RUN cd src && python3 manage.py collectstatic --noinput 2>/dev/null || true
RUN cd src && python3 manage.py compilemessages 2>/dev/null || true
# Purge build dependencies
RUN apt-get purge -y build-essential pkg-config libpq-dev \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
# Volumes
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8000 || exit 1
LABEL org.opencontainers.image.title="Paperless-ngx"
LABEL org.opencontainers.image.description="Self-hosted document management system"
LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}"
LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops"
LABEL org.opencontainers.image.vendor="blumeops"
ENTRYPOINT ["/init"]

View file

@ -0,0 +1 @@
Deploy Paperless-ngx document management system at paperless.ops.eblu.me with OCR, Authentik SSO, and NFS storage on sifaka.

View file

@ -41,6 +41,7 @@ DNS points to [[indri]]'s Tailscale IP. TLS via Let's Encrypt (ACME DNS-01 with
| [[jellyfin]] | https://jellyfin.ops.eblu.me | Media server |
| [[postgresql]] | pg.ops.eblu.me:5432 | Database |
| [[mealie]] | https://meals.ops.eblu.me | Recipe manager |
| [[paperless]] | https://paperless.ops.eblu.me | Document management |
| [[sifaka|Sifaka]] | https://nas.ops.eblu.me | NAS dashboard |
## Public Services (`*.eblu.me`)

View file

@ -40,6 +40,7 @@ Registry of all applications deployed via [[argocd]].
| `forgejo-runner` | forgejo-runner | `argocd/manifests/forgejo-runner/` | [[forgejo]] CI |
| `ollama` | ollama | `argocd/manifests/ollama/` | [[ollama]] |
| `mealie` | mealie | `argocd/manifests/mealie/` | [[mealie]] |
| `paperless` | paperless | `argocd/manifests/paperless/` | [[paperless]] |
| `prowler` | prowler | `argocd/manifests/prowler/` | [[prowler]] |
## Sync Policies

View file

@ -0,0 +1,45 @@
---
title: Paperless-ngx
modified: 2026-04-08
tags:
- service
---
# Paperless-ngx
Self-hosted document management system with OCR, tagging, and full-text search.
## Quick Reference
| Property | Value |
|----------|-------|
| **URL** | https://paperless.ops.eblu.me |
| **Tailscale URL** | https://paperless.tail8d86e.ts.net |
| **Namespace** | `paperless` |
| **Image** | `registry.ops.eblu.me/blumeops/paperless` |
| **Manifests** | `argocd/manifests/paperless/` |
| **Container source** | `containers/paperless/Dockerfile` |
| **Upstream** | [paperless-ngx/paperless-ngx](https://github.com/paperless-ngx/paperless-ngx) |
| **Database** | `paperless` on [[postgresql|blumeops-pg]] |
| **Storage** | NFS on [[sifaka]] at `/volume1/paperless` |
| **Auth** | [[authentik]] OIDC + local admin |
## Architecture
- **Web server**: Granian (ASGI), port 8000
- **Task queue**: Celery worker + beat (Redis sidecar)
- **OCR**: Tesseract (English)
- **Process supervisor**: s6-overlay
## Secrets
1Password item "Paperless (blumeops)" in vault `blumeops`:
- `secret-key`: Django SECRET_KEY
- `postgresql-password`: database credential
- `admin-password`: initial admin account password
- `socialaccount-providers`: OIDC provider JSON (includes Authentik client secret)
## Related
- [[adding-a-service]] — Deployment tutorial
- [[authentik]] — SSO provider

View file

@ -294,6 +294,13 @@ services:
upstream-source: https://github.com/mealie-recipes/mealie/releases
notes: Recipe manager; built from source via forge mirror
- name: paperless
type: argocd
last-reviewed: "2026-04-08"
current-version: "v2.20.13"
upstream-source: https://github.com/paperless-ngx/paperless-ngx/releases
notes: Document management; built from source via forge mirror
- name: unpoller
type: argocd
last-reviewed: 2026-03-16