From 07f52e9488430944d7c2bbbf62c087a86a5eb616 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 8 Apr 2026 17:54:12 -0700 Subject: [PATCH] Deploy Paperless-ngx document management (#328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: https://forge.eblu.me/eblume/blumeops/pulls/328 --- ansible/roles/caddy/defaults/main.yml | 3 + argocd/apps/paperless.yaml | 17 ++ .../authentik/configmap-blueprint.yaml | 44 +++++ .../authentik/deployment-worker.yaml | 5 + .../manifests/authentik/external-secret.yaml | 4 + argocd/manifests/databases/blumeops-pg.yaml | 8 + .../databases/external-secret-paperless.yaml | 28 ++++ argocd/manifests/databases/kustomization.yaml | 1 + argocd/manifests/paperless/deployment.yaml | 130 +++++++++++++++ .../manifests/paperless/external-secret.yaml | 31 ++++ .../paperless/ingress-tailscale.yaml | 25 +++ argocd/manifests/paperless/kustomization.yaml | 21 +++ argocd/manifests/paperless/pv-nfs.yaml | 22 +++ argocd/manifests/paperless/pvc.yaml | 15 ++ argocd/manifests/paperless/service.yaml | 13 ++ containers/paperless/Dockerfile | 156 ++++++++++++++++++ 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 + 21 files changed, 578 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/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 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..cc2c013 --- /dev/null +++ b/argocd/manifests/paperless/deployment.yaml @@ -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: {} 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..0810a44 --- /dev/null +++ b/argocd/manifests/paperless/kustomization.yaml @@ -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 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..a7b4e65 --- /dev/null +++ b/containers/paperless/Dockerfile @@ -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" 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