From 041c47acfb61494f112f0a97563b70289aa3294a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 16:33:34 -0700 Subject: [PATCH] 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) --- ansible/roles/caddy/defaults/main.yml | 3 + argocd/apps/paperless.yaml | 17 +++ argocd/manifests/databases/blumeops-pg.yaml | 8 ++ .../databases/external-secret-paperless.yaml | 28 ++++ argocd/manifests/databases/kustomization.yaml | 1 + argocd/manifests/paperless/deployment.yaml | 124 +++++++++++++++++ .../manifests/paperless/external-secret.yaml | 31 +++++ .../paperless/ingress-tailscale.yaml | 25 ++++ argocd/manifests/paperless/kustomization.yaml | 19 +++ argocd/manifests/paperless/pv-nfs.yaml | 22 +++ argocd/manifests/paperless/pvc.yaml | 15 ++ argocd/manifests/paperless/service.yaml | 13 ++ containers/paperless/Dockerfile | 128 ++++++++++++++++++ docs/changelog.d/deploy-paperless.feature.md | 1 + docs/reference/infrastructure/routing.md | 1 + docs/reference/kubernetes/apps.md | 1 + docs/reference/services/paperless.md | 45 ++++++ service-versions.yaml | 7 + 18 files changed, 489 insertions(+) create mode 100644 argocd/apps/paperless.yaml create mode 100644 argocd/manifests/databases/external-secret-paperless.yaml create mode 100644 argocd/manifests/paperless/deployment.yaml create mode 100644 argocd/manifests/paperless/external-secret.yaml create mode 100644 argocd/manifests/paperless/ingress-tailscale.yaml create mode 100644 argocd/manifests/paperless/kustomization.yaml create mode 100644 argocd/manifests/paperless/pv-nfs.yaml create mode 100644 argocd/manifests/paperless/pvc.yaml create mode 100644 argocd/manifests/paperless/service.yaml create mode 100644 containers/paperless/Dockerfile create mode 100644 docs/changelog.d/deploy-paperless.feature.md create mode 100644 docs/reference/services/paperless.md diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index f8f9156..ebb210b 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -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" diff --git a/argocd/apps/paperless.yaml b/argocd/apps/paperless.yaml new file mode 100644 index 0000000..88437eb --- /dev/null +++ b/argocd/apps/paperless.yaml @@ -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 diff --git a/argocd/manifests/databases/blumeops-pg.yaml b/argocd/manifests/databases/blumeops-pg.yaml index 8f1b878..58c771a 100644 --- a/argocd/manifests/databases/blumeops-pg.yaml +++ b/argocd/manifests/databases/blumeops-pg.yaml @@ -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: diff --git a/argocd/manifests/databases/external-secret-paperless.yaml b/argocd/manifests/databases/external-secret-paperless.yaml new file mode 100644 index 0000000..e5742be --- /dev/null +++ b/argocd/manifests/databases/external-secret-paperless.yaml @@ -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 diff --git a/argocd/manifests/databases/kustomization.yaml b/argocd/manifests/databases/kustomization.yaml index 68d28b2..b25e09e 100644 --- a/argocd/manifests/databases/kustomization.yaml +++ b/argocd/manifests/databases/kustomization.yaml @@ -14,3 +14,4 @@ resources: - external-secret-immich-borgmatic.yaml - external-secret-teslamate.yaml - external-secret-authentik.yaml + - external-secret-paperless.yaml diff --git a/argocd/manifests/paperless/deployment.yaml b/argocd/manifests/paperless/deployment.yaml new file mode 100644 index 0000000..777b9f0 --- /dev/null +++ b/argocd/manifests/paperless/deployment.yaml @@ -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: {} diff --git a/argocd/manifests/paperless/external-secret.yaml b/argocd/manifests/paperless/external-secret.yaml new file mode 100644 index 0000000..750b7c5 --- /dev/null +++ b/argocd/manifests/paperless/external-secret.yaml @@ -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 diff --git a/argocd/manifests/paperless/ingress-tailscale.yaml b/argocd/manifests/paperless/ingress-tailscale.yaml new file mode 100644 index 0000000..d09ef67 --- /dev/null +++ b/argocd/manifests/paperless/ingress-tailscale.yaml @@ -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 diff --git a/argocd/manifests/paperless/kustomization.yaml b/argocd/manifests/paperless/kustomization.yaml new file mode 100644 index 0000000..be77264 --- /dev/null +++ b/argocd/manifests/paperless/kustomization.yaml @@ -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 diff --git a/argocd/manifests/paperless/pv-nfs.yaml b/argocd/manifests/paperless/pv-nfs.yaml new file mode 100644 index 0000000..8ee7526 --- /dev/null +++ b/argocd/manifests/paperless/pv-nfs.yaml @@ -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 diff --git a/argocd/manifests/paperless/pvc.yaml b/argocd/manifests/paperless/pvc.yaml new file mode 100644 index 0000000..4365c9f --- /dev/null +++ b/argocd/manifests/paperless/pvc.yaml @@ -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 diff --git a/argocd/manifests/paperless/service.yaml b/argocd/manifests/paperless/service.yaml new file mode 100644 index 0000000..cff2972 --- /dev/null +++ b/argocd/manifests/paperless/service.yaml @@ -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 diff --git a/containers/paperless/Dockerfile b/containers/paperless/Dockerfile new file mode 100644 index 0000000..35df533 --- /dev/null +++ b/containers/paperless/Dockerfile @@ -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"] diff --git a/docs/changelog.d/deploy-paperless.feature.md b/docs/changelog.d/deploy-paperless.feature.md new file mode 100644 index 0000000..07b7899 --- /dev/null +++ b/docs/changelog.d/deploy-paperless.feature.md @@ -0,0 +1 @@ +Deploy Paperless-ngx document management system at paperless.ops.eblu.me with OCR, Authentik SSO, and NFS storage on sifaka. diff --git a/docs/reference/infrastructure/routing.md b/docs/reference/infrastructure/routing.md index c85dbb5..a8049d6 100644 --- a/docs/reference/infrastructure/routing.md +++ b/docs/reference/infrastructure/routing.md @@ -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`) diff --git a/docs/reference/kubernetes/apps.md b/docs/reference/kubernetes/apps.md index e162c7a..80ea72e 100644 --- a/docs/reference/kubernetes/apps.md +++ b/docs/reference/kubernetes/apps.md @@ -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 diff --git a/docs/reference/services/paperless.md b/docs/reference/services/paperless.md new file mode 100644 index 0000000..c74543e --- /dev/null +++ b/docs/reference/services/paperless.md @@ -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 diff --git a/service-versions.yaml b/service-versions.yaml index 64d043a..9a94c6a 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -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