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
|
|
@ -92,6 +92,9 @@ caddy_services:
|
||||||
- name: mealie
|
- name: mealie
|
||||||
host: "meals.{{ caddy_domain }}"
|
host: "meals.{{ caddy_domain }}"
|
||||||
backend: "https://meals.tail8d86e.ts.net"
|
backend: "https://meals.tail8d86e.ts.net"
|
||||||
|
- name: paperless
|
||||||
|
host: "paperless.{{ caddy_domain }}"
|
||||||
|
backend: "https://paperless.tail8d86e.ts.net"
|
||||||
- name: sifaka
|
- name: sifaka
|
||||||
host: "nas.{{ caddy_domain }}"
|
host: "nas.{{ caddy_domain }}"
|
||||||
backend: "http://sifaka:5000"
|
backend: "http://sifaka:5000"
|
||||||
|
|
|
||||||
17
argocd/apps/paperless.yaml
Normal file
17
argocd/apps/paperless.yaml
Normal 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
|
||||||
|
|
@ -346,6 +346,50 @@ data:
|
||||||
meta_launch_url: https://jellyfin.ops.eblu.me
|
meta_launch_url: https://jellyfin.ops.eblu.me
|
||||||
policy_engine_mode: all
|
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: |
|
mealie.yaml: |
|
||||||
version: 1
|
version: 1
|
||||||
metadata:
|
metadata:
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,11 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: authentik-config
|
name: authentik-config
|
||||||
key: mealie-client-secret
|
key: mealie-client-secret
|
||||||
|
- name: AUTHENTIK_PAPERLESS_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: authentik-config
|
||||||
|
key: paperless-client-secret
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: blueprints
|
- name: blueprints
|
||||||
mountPath: /blueprints/custom
|
mountPath: /blueprints/custom
|
||||||
|
|
|
||||||
|
|
@ -61,3 +61,7 @@ spec:
|
||||||
remoteRef:
|
remoteRef:
|
||||||
key: "Authentik (blumeops)"
|
key: "Authentik (blumeops)"
|
||||||
property: mealie-client-secret
|
property: mealie-client-secret
|
||||||
|
- secretKey: paperless-client-secret
|
||||||
|
remoteRef:
|
||||||
|
key: "Authentik (blumeops)"
|
||||||
|
property: paperless-client-secret
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,14 @@ spec:
|
||||||
createdb: true
|
createdb: true
|
||||||
passwordSecret:
|
passwordSecret:
|
||||||
name: blumeops-pg-authentik
|
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
|
# Resource limits for minikube environment
|
||||||
resources:
|
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-immich-borgmatic.yaml
|
||||||
- external-secret-teslamate.yaml
|
- external-secret-teslamate.yaml
|
||||||
- external-secret-authentik.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
|
||||||
156
containers/paperless/Dockerfile
Normal file
156
containers/paperless/Dockerfile
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# Paperless-ngx — self-hosted document management
|
||||||
|
# Built from source via forge mirror of paperless-ngx/paperless-ngx
|
||||||
|
# Closely follows upstream Dockerfile structure with git clone instead of COPY
|
||||||
|
|
||||||
|
ARG CONTAINER_APP_VERSION=v2.20.13
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Stage 1: Clone source (reused by later stages)
|
||||||
|
###############################################
|
||||||
|
FROM docker.io/library/alpine:3.22 AS source
|
||||||
|
|
||||||
|
ARG CONTAINER_APP_VERSION
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
|
||||||
|
https://forge.ops.eblu.me/mirrors/paperless-ngx.git /src
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Stage 2: Compile frontend
|
||||||
|
###############################################
|
||||||
|
FROM --platform=$BUILDPLATFORM docker.io/node:20-trixie-slim AS compile-frontend
|
||||||
|
|
||||||
|
COPY --from=source /src/src-ui /src/src-ui
|
||||||
|
WORKDIR /src/src-ui
|
||||||
|
|
||||||
|
RUN set -eux \
|
||||||
|
&& npm update -g pnpm \
|
||||||
|
&& npm install -g corepack@latest \
|
||||||
|
&& corepack enable \
|
||||||
|
&& pnpm install
|
||||||
|
|
||||||
|
RUN set -eux \
|
||||||
|
&& ./node_modules/.bin/ng build --configuration production
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Stage 3: s6-overlay base
|
||||||
|
###############################################
|
||||||
|
FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
|
||||||
|
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
|
||||||
|
S6_VERBOSITY=1 \
|
||||||
|
PATH=/command:$PATH
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
ARG S6_OVERLAY_VERSION=3.2.1.0
|
||||||
|
|
||||||
|
RUN set -eux \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install --yes --quiet --no-install-recommends curl xz-utils \
|
||||||
|
&& S6_ARCH="" \
|
||||||
|
&& if [ "${TARGETARCH}${TARGETVARIANT}" = "amd64" ]; then S6_ARCH="x86_64"; \
|
||||||
|
elif [ "${TARGETARCH}${TARGETVARIANT}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
|
||||||
|
&& if [ -z "${S6_ARCH}" ]; then echo "Error: Cannot determine arch"; exit 1; fi \
|
||||||
|
&& curl --fail --silent --show-error --location --remote-name-all --parallel \
|
||||||
|
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \
|
||||||
|
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" \
|
||||||
|
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \
|
||||||
|
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" \
|
||||||
|
&& sha256sum --check ./*.sha256 \
|
||||||
|
&& tar --directory / -Jxpf s6-overlay-noarch.tar.xz \
|
||||||
|
&& tar --directory / -Jxpf s6-overlay-${S6_ARCH}.tar.xz \
|
||||||
|
&& rm ./*.tar.xz ./*.sha256 \
|
||||||
|
&& apt-get --yes purge curl xz-utils \
|
||||||
|
&& apt-get --yes autoremove --purge \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy rootfs (s6 service definitions, init scripts)
|
||||||
|
COPY --from=source /src/docker/rootfs /
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Stage 4: Main application
|
||||||
|
###############################################
|
||||||
|
FROM s6-overlay-base AS main-app
|
||||||
|
|
||||||
|
ARG CONTAINER_APP_VERSION
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG JBIG2ENC_VERSION=0.30
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
||||||
|
PNGX_CONTAINERIZED=1 \
|
||||||
|
UV_LINK_MODE=copy \
|
||||||
|
UV_CACHE_DIR=/cache/uv/
|
||||||
|
|
||||||
|
# Runtime packages
|
||||||
|
RUN set -eux \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install --yes --quiet --no-install-recommends \
|
||||||
|
curl gosu tzdata fonts-liberation gettext ghostscript gnupg \
|
||||||
|
icc-profiles-free imagemagick postgresql-client \
|
||||||
|
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
||||||
|
tesseract-ocr-ita tesseract-ocr-spa unpaper pngquant jbig2dec \
|
||||||
|
libxml2 libxslt1.1 qpdf file libmagic1 media-types zlib1g \
|
||||||
|
libzbar0 poppler-utils \
|
||||||
|
&& curl --fail --silent --show-error --location --remote-name-all \
|
||||||
|
"https://github.com/paperless-ngx/builder/releases/download/jbig2enc-trixie-v${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb" \
|
||||||
|
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||||
|
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
|
||||||
|
&& rm --force *.deb \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /usr/src/paperless/src/
|
||||||
|
|
||||||
|
# Python dependencies
|
||||||
|
COPY --from=source /src/pyproject.toml /src/uv.lock /usr/src/paperless/src/
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=${UV_CACHE_DIR},id=python-cache \
|
||||||
|
set -eux \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install --yes --quiet --no-install-recommends \
|
||||||
|
build-essential default-libmysqlclient-dev pkg-config \
|
||||||
|
&& uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \
|
||||||
|
&& uv pip install --system --no-python-downloads --python-preference system --requirements requirements.txt \
|
||||||
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||||
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||||
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt_tab \
|
||||||
|
&& apt-get --yes purge build-essential default-libmysqlclient-dev pkg-config \
|
||||||
|
&& apt-get --yes autoremove --purge \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# Copy backend source
|
||||||
|
COPY --from=source /src/src ./
|
||||||
|
|
||||||
|
# Copy compiled frontend
|
||||||
|
COPY --from=compile-frontend /src/src/documents/static/frontend/ ./documents/static/frontend/
|
||||||
|
|
||||||
|
# Create user and finalize
|
||||||
|
RUN set -eux \
|
||||||
|
&& addgroup --gid 1000 paperless \
|
||||||
|
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||||
|
&& mkdir -p /usr/src/paperless/data /usr/src/paperless/media \
|
||||||
|
/usr/src/paperless/consume /usr/src/paperless/export \
|
||||||
|
&& chown -R paperless:paperless /usr/src/paperless \
|
||||||
|
&& s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||||
|
&& s6-setuidgid paperless python3 manage.py compilemessages
|
||||||
|
|
||||||
|
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", \
|
||||||
|
"/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["/init"]
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --retries=5 \
|
||||||
|
CMD [ "curl", "-fs", "-S", "-L", "--max-time", "2", "http://localhost:8000" ]
|
||||||
|
|
||||||
|
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"
|
||||||
1
docs/changelog.d/deploy-paperless.feature.md
Normal file
1
docs/changelog.d/deploy-paperless.feature.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Deploy Paperless-ngx document management system at paperless.ops.eblu.me with OCR, Authentik SSO, and NFS storage on sifaka.
|
||||||
|
|
@ -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 |
|
| [[jellyfin]] | https://jellyfin.ops.eblu.me | Media server |
|
||||||
| [[postgresql]] | pg.ops.eblu.me:5432 | Database |
|
| [[postgresql]] | pg.ops.eblu.me:5432 | Database |
|
||||||
| [[mealie]] | https://meals.ops.eblu.me | Recipe manager |
|
| [[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 |
|
| [[sifaka|Sifaka]] | https://nas.ops.eblu.me | NAS dashboard |
|
||||||
|
|
||||||
## Public Services (`*.eblu.me`)
|
## Public Services (`*.eblu.me`)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ Registry of all applications deployed via [[argocd]].
|
||||||
| `forgejo-runner` | forgejo-runner | `argocd/manifests/forgejo-runner/` | [[forgejo]] CI |
|
| `forgejo-runner` | forgejo-runner | `argocd/manifests/forgejo-runner/` | [[forgejo]] CI |
|
||||||
| `ollama` | ollama | `argocd/manifests/ollama/` | [[ollama]] |
|
| `ollama` | ollama | `argocd/manifests/ollama/` | [[ollama]] |
|
||||||
| `mealie` | mealie | `argocd/manifests/mealie/` | [[mealie]] |
|
| `mealie` | mealie | `argocd/manifests/mealie/` | [[mealie]] |
|
||||||
|
| `paperless` | paperless | `argocd/manifests/paperless/` | [[paperless]] |
|
||||||
| `prowler` | prowler | `argocd/manifests/prowler/` | [[prowler]] |
|
| `prowler` | prowler | `argocd/manifests/prowler/` | [[prowler]] |
|
||||||
|
|
||||||
## Sync Policies
|
## Sync Policies
|
||||||
|
|
|
||||||
45
docs/reference/services/paperless.md
Normal file
45
docs/reference/services/paperless.md
Normal 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
|
||||||
|
|
@ -294,6 +294,13 @@ services:
|
||||||
upstream-source: https://github.com/mealie-recipes/mealie/releases
|
upstream-source: https://github.com/mealie-recipes/mealie/releases
|
||||||
notes: Recipe manager; built from source via forge mirror
|
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
|
- name: unpoller
|
||||||
type: argocd
|
type: argocd
|
||||||
last-reviewed: 2026-03-16
|
last-reviewed: 2026-03-16
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue