Deploy Paperless-ngx document management (#328)
## 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:
parent
e04455c911
commit
07f52e9488
21 changed files with 578 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
28
argocd/manifests/databases/external-secret-paperless.yaml
Normal file
28
argocd/manifests/databases/external-secret-paperless.yaml
Normal 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
|
||||
|
|
@ -14,3 +14,4 @@ resources:
|
|||
- external-secret-immich-borgmatic.yaml
|
||||
- external-secret-teslamate.yaml
|
||||
- external-secret-authentik.yaml
|
||||
- external-secret-paperless.yaml
|
||||
|
|
|
|||
130
argocd/manifests/paperless/deployment.yaml
Normal file
130
argocd/manifests/paperless/deployment.yaml
Normal 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: {}
|
||||
31
argocd/manifests/paperless/external-secret.yaml
Normal file
31
argocd/manifests/paperless/external-secret.yaml
Normal 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
|
||||
25
argocd/manifests/paperless/ingress-tailscale.yaml
Normal file
25
argocd/manifests/paperless/ingress-tailscale.yaml
Normal 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
|
||||
21
argocd/manifests/paperless/kustomization.yaml
Normal file
21
argocd/manifests/paperless/kustomization.yaml
Normal 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
|
||||
22
argocd/manifests/paperless/pv-nfs.yaml
Normal file
22
argocd/manifests/paperless/pv-nfs.yaml
Normal 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
|
||||
15
argocd/manifests/paperless/pvc.yaml
Normal file
15
argocd/manifests/paperless/pvc.yaml
Normal 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
|
||||
13
argocd/manifests/paperless/service.yaml
Normal file
13
argocd/manifests/paperless/service.yaml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue