Deploy Paperless-ngx document management (#328)
All checks were successful
Build Container / detect (push) Successful in 2s
Build Container / build-dockerfile (paperless) (push) Successful in 9s

## Summary

- Add paperless-ngx (v2.20.13) as a new ArgoCD-managed service on indri
- Dockerfile built from forge mirror (`mirrors/paperless-ngx`), multi-stage with s6-overlay
- PostgreSQL database via `blumeops-pg` CNPG cluster, Redis sidecar for Celery
- NFS document storage on sifaka (`/volume1/paperless`)
- Authentik OIDC SSO via baked JSON blob from 1Password
- Caddy route at `paperless.ops.eblu.me`
- 1Password item "Paperless (blumeops)" created with all secrets

## Files

- `containers/paperless/Dockerfile` — multi-stage build
- `argocd/manifests/paperless/` — full k8s manifest set
- `argocd/apps/paperless.yaml` — ArgoCD application
- `argocd/manifests/databases/` — CNPG role + ExternalSecret
- `ansible/roles/caddy/defaults/main.yml` — Caddy route
- `service-versions.yaml` — version tracking entry
- `docs/reference/services/paperless.md` — reference card

## Remaining deploy steps

1. Build container: `mise run container-build-and-release paperless`
2. Update kustomization.yaml `newTag` with actual image tag
3. Create Authentik application/provider for paperless
4. Create `paperless` database on blumeops-pg
5. Sync ArgoCD apps, then sync paperless from branch
6. Provision Caddy: `mise run provision-indri -- --tags caddy`
7. Verify at https://paperless.ops.eblu.me

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #328
This commit is contained in:
Erich Blume 2026-04-08 17:54:12 -07:00
commit 07f52e9488
21 changed files with 578 additions and 0 deletions

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

@ -346,6 +346,50 @@ data:
meta_launch_url: https://jellyfin.ops.eblu.me
policy_engine_mode: all
paperless.yaml: |
version: 1
metadata:
name: BlumeOps Paperless SSO
labels:
blueprints.goauthentik.io/description: "Paperless-ngx OIDC provider and application"
entries:
# OAuth2 provider for Paperless-ngx (confidential client)
- model: authentik_providers_oauth2.oauth2provider
id: paperless-provider
identifiers:
name: Paperless
attrs:
name: Paperless
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
client_type: confidential
client_id: paperless
client_secret: !Env AUTHENTIK_PAPERLESS_CLIENT_SECRET
redirect_uris:
- matching_mode: strict
url: https://paperless.ops.eblu.me/accounts/oidc/authentik/login/callback/
- matching_mode: strict
url: https://paperless.tail8d86e.ts.net/accounts/oidc/authentik/login/callback/
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
sub_mode: hashed_user_id
include_claims_in_id_token: true
# Paperless application — all authenticated users allowed
- model: authentik_core.application
id: paperless-app
identifiers:
slug: paperless
attrs:
name: Paperless
slug: paperless
provider: !KeyOf paperless-provider
meta_launch_url: https://paperless.ops.eblu.me
policy_engine_mode: all
mealie.yaml: |
version: 1
metadata:

View file

@ -85,6 +85,11 @@ spec:
secretKeyRef:
name: authentik-config
key: mealie-client-secret
- name: AUTHENTIK_PAPERLESS_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: authentik-config
key: paperless-client-secret
volumeMounts:
- name: blueprints
mountPath: /blueprints/custom

View file

@ -61,3 +61,7 @@ spec:
remoteRef:
key: "Authentik (blumeops)"
property: mealie-client-secret
- secretKey: paperless-client-secret
remoteRef:
key: "Authentik (blumeops)"
property: paperless-client-secret

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,130 @@
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"
# Explicit port to override k8s-injected PAPERLESS_PORT env var
# (k8s sets PAPERLESS_PORT=tcp://... for a service named 'paperless')
- name: PAPERLESS_PORT
value: "8000"
- 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: "eblume"
- name: PAPERLESS_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: paperless-secrets
key: admin-password
- name: PAPERLESS_ADMIN_MAIL
value: "blume.erich@gmail.com"
# 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_ACCOUNT_ALLOW_SIGNUPS
value: "false"
- 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,21 @@
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: v2.20.13-42f6299
# TODO: borrowing authentik-redis image — consider building a generic
# blumeops/redis container if more services need Redis sidecars
- name: docker.io/library/redis
newName: registry.ops.eblu.me/blumeops/authentik-redis
newTag: v8.2.3-fd0bebb-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