From 041c47acfb61494f112f0a97563b70289aa3294a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 16:33:34 -0700 Subject: [PATCH 1/8] 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 From d036782b43a21303ae21116655ad6abb7ce0f32d Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 16:38:57 -0700 Subject: [PATCH 2/8] Fix paperless admin username and email Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/paperless/deployment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/paperless/deployment.yaml b/argocd/manifests/paperless/deployment.yaml index 777b9f0..5cf4f69 100644 --- a/argocd/manifests/paperless/deployment.yaml +++ b/argocd/manifests/paperless/deployment.yaml @@ -53,14 +53,14 @@ spec: value: "1" # Admin account (created on first startup) - name: PAPERLESS_ADMIN_USER - value: "erich" + value: "eblume" - name: PAPERLESS_ADMIN_PASSWORD valueFrom: secretKeyRef: name: paperless-secrets key: admin-password - name: PAPERLESS_ADMIN_MAIL - value: "erich@eblu.me" + value: "blume.erich@gmail.com" # OIDC via Authentik # Full JSON blob pulled from 1Password (includes client secret) - name: PAPERLESS_APPS From fba339e543319c516a35a18dadcc1f84dd0be1d9 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 16:44:11 -0700 Subject: [PATCH 3/8] Fix jbig2enc download: ADD doesn't expand shell subcommands Use curl in a RUN instead of ADD so $(dpkg --print-architecture) is evaluated by the shell. Co-Authored-By: Claude Opus 4.6 (1M context) --- containers/paperless/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/containers/paperless/Dockerfile b/containers/paperless/Dockerfile index 35df533..24b22da 100644 --- a/containers/paperless/Dockerfile +++ b/containers/paperless/Dockerfile @@ -65,8 +65,10 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ && 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 +RUN curl -fsSL "https://github.com/paperless-ngx/builder/releases/download/jbig2enc-0.29/jbig2enc_0.29-1_$(dpkg --print-architecture).deb" -o /tmp/jbig2enc.deb \ + && dpkg -i /tmp/jbig2enc.deb \ + && rm /tmp/jbig2enc.deb \ + || true WORKDIR /usr/src/paperless From 42f6299eaa2e742d3dd34bd91fc0f46ef66fd548 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 17:03:31 -0700 Subject: [PATCH 4/8] Rewrite paperless Dockerfile to match upstream structure Add syntax directive for BuildKit, use COPY --from=source instead of inline git clone, fix s6-overlay arch mapping, use upstream jbig2enc v0.30 trixie build, and enable RUN --mount=type=cache for Python deps. Co-Authored-By: Claude Opus 4.6 (1M context) --- containers/paperless/Dockerfile | 202 ++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 88 deletions(-) diff --git a/containers/paperless/Dockerfile b/containers/paperless/Dockerfile index 24b22da..a7b4e65 100644 --- a/containers/paperless/Dockerfile +++ b/containers/paperless/Dockerfile @@ -1,130 +1,156 @@ +# syntax=docker/dockerfile:1 # 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) +# Closely follows upstream Dockerfile structure with git clone instead of COPY ARG CONTAINER_APP_VERSION=v2.20.13 ############################################### -# Frontend Build +# Stage 1: Clone source (reused by later stages) ############################################### -FROM node:20-slim AS frontend-builder +FROM docker.io/library/alpine:3.22 AS source 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 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 corepack enable && corepack prepare pnpm@latest --activate -RUN pnpm install --frozen-lockfile -RUN ./node_modules/.bin/ng build --configuration production + +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 ############################################### -# s6-overlay base +# Stage 3: s6-overlay base ############################################### -FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-base +FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-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 +WORKDIR /usr/src/s6 ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \ - S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + 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 / ############################################### -# Main Application +# Stage 4: Main application ############################################### -FROM s6-base AS production +FROM s6-overlay-base AS main-app ARG CONTAINER_APP_VERSION +ARG DEBIAN_FRONTEND=noninteractive +ARG TARGETARCH +ARG JBIG2ENC_VERSION=0.30 -# 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 \ +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/* -# Install jbig2enc from upstream prebuilt deb -RUN curl -fsSL "https://github.com/paperless-ngx/builder/releases/download/jbig2enc-0.29/jbig2enc_0.29-1_$(dpkg --print-architecture).deb" -o /tmp/jbig2enc.deb \ - && dpkg -i /tmp/jbig2enc.deb \ - && rm /tmp/jbig2enc.deb \ - || true +WORKDIR /usr/src/paperless/src/ -WORKDIR /usr/src/paperless +# Python dependencies +COPY --from=source /src/pyproject.toml /src/uv.lock /usr/src/paperless/src/ -# 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/* +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 frontend build -COPY --from=frontend-builder /src/src/documents/static/frontend/ /usr/src/paperless/src/documents/static/frontend/ +# Copy backend source +COPY --from=source /src/src ./ -# 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]" +# Copy compiled frontend +COPY --from=compile-frontend /src/src/documents/static/frontend/ ./documents/static/frontend/ -# 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 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 -# 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"] +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=3 \ - CMD curl -f http://localhost:8000 || exit 1 +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" - -ENTRYPOINT ["/init"] From 919ad04e302fa23b996bca8a910d987cb8a99cdf Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 17:34:17 -0700 Subject: [PATCH 5/8] Pin paperless and redis image tags from registry Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/paperless/kustomization.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/paperless/kustomization.yaml b/argocd/manifests/paperless/kustomization.yaml index be77264..0810a44 100644 --- a/argocd/manifests/paperless/kustomization.yaml +++ b/argocd/manifests/paperless/kustomization.yaml @@ -13,7 +13,9 @@ resources: images: - name: registry.ops.eblu.me/blumeops/paperless - newTag: placeholder + 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-e04455c-nix + newTag: v8.2.3-fd0bebb-nix From 0bde34d6e1c66c46b159ea72c34ea249e741a8d4 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 17:39:53 -0700 Subject: [PATCH 6/8] Fix paperless port: override k8s-injected PAPERLESS_PORT env var Kubernetes auto-injects PAPERLESS_PORT=tcp://... for a service named 'paperless', which conflicts with Granian's --port flag. Explicitly set PAPERLESS_PORT=8000 to take precedence. Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/paperless/deployment.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/argocd/manifests/paperless/deployment.yaml b/argocd/manifests/paperless/deployment.yaml index 5cf4f69..7eb2805 100644 --- a/argocd/manifests/paperless/deployment.yaml +++ b/argocd/manifests/paperless/deployment.yaml @@ -33,6 +33,10 @@ spec: 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 From ba5c3a6bae27aba3f52cc4e69c829c8f76a14697 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 17:49:24 -0700 Subject: [PATCH 7/8] Add Authentik OIDC provider and application for Paperless MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blueprint with confidential client, ExternalSecret for client secret, and worker env var injection — follows existing service pattern. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../authentik/configmap-blueprint.yaml | 44 +++++++++++++++++++ .../authentik/deployment-worker.yaml | 5 +++ .../manifests/authentik/external-secret.yaml | 4 ++ 3 files changed, 53 insertions(+) diff --git a/argocd/manifests/authentik/configmap-blueprint.yaml b/argocd/manifests/authentik/configmap-blueprint.yaml index cc3ff43..27910ef 100644 --- a/argocd/manifests/authentik/configmap-blueprint.yaml +++ b/argocd/manifests/authentik/configmap-blueprint.yaml @@ -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: diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml index 5fe473e..b81ec32 100644 --- a/argocd/manifests/authentik/deployment-worker.yaml +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -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 diff --git a/argocd/manifests/authentik/external-secret.yaml b/argocd/manifests/authentik/external-secret.yaml index fb22f2b..9abf699 100644 --- a/argocd/manifests/authentik/external-secret.yaml +++ b/argocd/manifests/authentik/external-secret.yaml @@ -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 From 3027fa6089024df33941aba646082fcbc216e42e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 17:50:49 -0700 Subject: [PATCH 8/8] Disable local self-service registration in paperless Users must be added via Authentik OIDC; eblume is the only local account. Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/paperless/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/argocd/manifests/paperless/deployment.yaml b/argocd/manifests/paperless/deployment.yaml index 7eb2805..cc2c013 100644 --- a/argocd/manifests/paperless/deployment.yaml +++ b/argocd/manifests/paperless/deployment.yaml @@ -78,6 +78,8 @@ spec: 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: