From fb5db18f00823a942ec9ba9fede0a21c16fdfd88 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Mon, 19 Oct 2026 21:22:12 +0200 Subject: [PATCH] Publish paperless-ngx-helm chart for upstream v3.3.2 --- .gitea/workflows/chart.yml | 71 +++++++++++++++ README.md | 27 ++++++ chart/paperless-ngx/Chart.yaml | 6 ++ chart/paperless-ngx/templates/deployment.yaml | 91 +++++++++++++++++++ chart/paperless-ngx/templates/ingress.yaml | 24 +++++ chart/paperless-ngx/templates/pvc.yaml | 33 +++++++ chart/paperless-ngx/templates/service.yaml | 7 ++ chart/paperless-ngx/values.schema.json | 13 +++ chart/paperless-ngx/values.yaml | 44 +++++++++ install.sh | 6 ++ scripts/chart-version.sh | 7 ++ test.sh | 18 ++++ values.example.yaml | 14 +++ 13 files changed, 361 insertions(+) create mode 100644 .gitea/workflows/chart.yml create mode 100644 README.md create mode 100644 chart/paperless-ngx/Chart.yaml create mode 100644 chart/paperless-ngx/templates/deployment.yaml create mode 100644 chart/paperless-ngx/templates/ingress.yaml create mode 100644 chart/paperless-ngx/templates/pvc.yaml create mode 100644 chart/paperless-ngx/templates/service.yaml create mode 100644 chart/paperless-ngx/values.schema.json create mode 100644 chart/paperless-ngx/values.yaml create mode 100755 install.sh create mode 100755 scripts/chart-version.sh create mode 100755 test.sh create mode 100644 values.example.yaml diff --git a/.gitea/workflows/chart.yml b/.gitea/workflows/chart.yml new file mode 100644 index 0000000..2515582 --- /dev/null +++ b/.gitea/workflows/chart.yml @@ -0,0 +1,71 @@ +name: Validate, publish and promote Paperless-ngx +on: + push: + pull_request: +jobs: + chart: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: azure/setup-helm@v4 + with: + version: v4.3.0 + - name: Validate chart + run: | + python3 -m venv "${RUNNER_TEMP}/chart-test" + "${RUNNER_TEMP}/chart-test/bin/pip" install PyYAML + PATH="${RUNNER_TEMP}/chart-test/bin:${PATH}" ./test.sh + - name: Select immutable chart version + if: gitea.ref == 'refs/heads/main' + run: printf 'CHART_VERSION=%s\n' "$(./scripts/chart-version.sh)" >> "${GITHUB_ENV}" + - name: Package and publish chart + if: gitea.ref == 'refs/heads/main' + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + run: | + set -euo pipefail + test -n "${PACKAGE_USERNAME}" && test -n "${PACKAGE_TOKEN}" + mkdir -p .chart-packages + helm package chart/paperless-ngx --destination .chart-packages --version "${CHART_VERSION}" + curl --fail --silent --show-error --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST --upload-file ".chart-packages/paperless-ngx-${CHART_VERSION}.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + - name: Check current main + id: current-main + if: gitea.ref == 'refs/heads/main' + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + git fetch --no-tags origin main + if test "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}"; then + echo 'promote=true' >> "${GITEA_OUTPUT}" + else + echo 'promote=false' >> "${GITEA_OUTPUT}" + fi + - uses: actions/checkout@v4 + if: steps.current-main.outputs.promote == 'true' + with: + repository: feedc0de/homelab-gitops + ref: main + token: ${{ secrets.GITOPS_TOKEN }} + path: homelab-gitops + - name: Promote chart + if: steps.current-main.outputs.promote == 'true' + working-directory: homelab-gitops + run: | + set -euo pipefail + ./scripts/update-chart-version.sh paperless "${CHART_VERSION}" + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + git add bootstrap/paperless.yaml + if git diff --cached --quiet; then exit 0; fi + git commit -m "Deploy paperless chart ${CHART_VERSION}" + for attempt in 1 2 3; do + git push origin HEAD:main && exit 0 + test "${attempt}" = 3 && exit 1 + git fetch origin main + git rebase origin/main + done diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b5d420 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Paperless-ngx Helm chart + +This independent chart packages the upstream Paperless-ngx image without bundling +PostgreSQL or Valkey. It declares the Paperless Deployment, Service, Ingress, +probes, Secret references, and optional PVCs. The chart Git tag follows the +upstream application release (`v3.3.2`); later chart-only commits append Git +describe's count and hash. The default image tag comes from `appVersion`. + +Supply existing Secret names for the Paperless key and PostgreSQL password. +The chart never renders credentials. Set `persistence.data.existingClaim` and +`persistence.runtime.existingClaim` to retain existing claims. The shared data +claim may also be mounted by a separate scanner release. Scheduling affinity, +tolerations, OCR worker count, and cluster hostname are values, not chart +constants. The homelab deployment is in +[homelab-gitops/apps/paperless](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/paperless). + +```sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +helm upgrade --install paperless brunner.ninja/paperless-ngx \ + --namespace paperless-ngx --create-namespace -f values.yaml --wait --timeout 15m +``` + +For local testing run `./test.sh`; `./install.sh values.yaml` uses the current +Kubernetes context and the chart in this checkout. CI publishes the chart, +then promotes only its chart version to GitOps. Required Actions secrets are +`PACKAGE_USERNAME`, `PACKAGE_TOKEN`, and `GITOPS_TOKEN`. diff --git a/chart/paperless-ngx/Chart.yaml b/chart/paperless-ngx/Chart.yaml new file mode 100644 index 0000000..5797f63 --- /dev/null +++ b/chart/paperless-ngx/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: paperless-ngx +description: Paperless-ngx application without a bundled database or cache +type: application +version: 0.0.0 +appVersion: 3.3.2 diff --git a/chart/paperless-ngx/templates/deployment.yaml b/chart/paperless-ngx/templates/deployment.yaml new file mode 100644 index 0000000..d770d8e --- /dev/null +++ b/chart/paperless-ngx/templates/deployment.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: {app: paperless} + template: + metadata: + labels: {app: paperless} + spec: + automountServiceAccountToken: false + enableServiceLinks: false + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + containers: + - name: paperless + image: {{ printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: [{name: http, containerPort: 8000}] + env: + - {name: PAPERLESS_URL, value: {{ .Values.url | quote }}} + - {name: PAPERLESS_REDIS, value: {{ .Values.cache.url | quote }}} + - {name: PAPERLESS_DBENGINE, value: postgresql} + - {name: PAPERLESS_DBHOST, value: {{ .Values.database.host | quote }}} + - {name: PAPERLESS_DBNAME, value: {{ .Values.database.name | quote }}} + - {name: PAPERLESS_DBUSER, value: {{ .Values.database.user | quote }}} + - name: PAPERLESS_DBPASS + valueFrom: + secretKeyRef: {name: {{ .Values.database.passwordSecretName }}, key: {{ .Values.database.passwordSecretKey }}} + - {name: PAPERLESS_TIME_ZONE, value: {{ .Values.timeZone | quote }}} + - {name: PAPERLESS_OCR_LANGUAGE, value: {{ .Values.ocrLanguage | quote }}} + - {name: PAPERLESS_TASK_WORKERS, value: {{ .Values.taskWorkers | quote }}} + - {name: PAPERLESS_THREADS_PER_WORKER, value: {{ .Values.threadsPerWorker | quote }}} + - {name: PAPERLESS_CONSUMER_POLLING_INTERVAL, value: {{ .Values.consumerPollingInterval | quote }}} + {{- if .Values.oidc.enabled }} + - {name: PAPERLESS_APPS, value: allauth.socialaccount.providers.openid_connect} + - {name: PAPERLESS_SOCIAL_AUTO_SIGNUP, value: "true"} + - {name: PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS, value: "true"} + - {name: PAPERLESS_SOCIAL_ACCOUNT_SYNC_SUPERUSER_GROUP, value: {{ .Values.oidc.adminGroup | quote }}} + - name: PAPERLESS_SOCIALACCOUNT_PROVIDERS + valueFrom: + secretKeyRef: {name: {{ .Values.oidc.providersSecretName }}, key: {{ .Values.oidc.providersSecretKey }}} + {{- end }} + - name: PAPERLESS_SECRET_KEY + valueFrom: + secretKeyRef: {name: {{ .Values.secretKey.secretName }}, key: {{ .Values.secretKey.secretKey }}} + volumeMounts: + - {name: runtime, mountPath: /usr/src/paperless/data} + - {name: data, mountPath: /usr/src/paperless/media, subPath: media} + - {name: data, mountPath: /usr/src/paperless/consume, subPath: consume} + - {name: data, mountPath: /usr/src/paperless/export, subPath: export} + startupProbe: + httpGet: {path: /, port: http} + periodSeconds: 10 + failureThreshold: 60 + timeoutSeconds: 10 + readinessProbe: + httpGet: {path: /, port: http} + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 6 + livenessProbe: + exec: + command: + - sh + - -ec + - | + for service in svc-worker svc-webserver svc-consumer svc-scheduler; do + pid="$(s6-svstat -o pid "/run/service/$service")" + [ "$pid" -gt 0 ] + done + periodSeconds: 60 + timeoutSeconds: 30 + failureThreshold: 30 + resources: +{{ toYaml .Values.resources | indent 12 }} + volumes: + - name: runtime + persistentVolumeClaim: {claimName: {{ .Values.persistence.runtime.existingClaim | default "paperless-runtime" }}} + - name: data + persistentVolumeClaim: {claimName: {{ .Values.persistence.data.existingClaim | default "paperless-data" }}} diff --git a/chart/paperless-ngx/templates/ingress.yaml b/chart/paperless-ngx/templates/ingress.yaml new file mode 100644 index 0000000..27959f4 --- /dev/null +++ b/chart/paperless-ngx/templates/ingress.yaml @@ -0,0 +1,24 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: paperless + {{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: paperless + port: {name: http} +{{- end }} diff --git a/chart/paperless-ngx/templates/pvc.yaml b/chart/paperless-ngx/templates/pvc.yaml new file mode 100644 index 0000000..f88be9f --- /dev/null +++ b/chart/paperless-ngx/templates/pvc.yaml @@ -0,0 +1,33 @@ +{{- if not .Values.persistence.data.existingClaim }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: paperless-data + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: [ReadWriteMany] + {{- with .Values.persistence.data.storageClassName }} + storageClassName: {{ . }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.data.size }} +--- +{{- end }} +{{- if not .Values.persistence.runtime.existingClaim }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: paperless-runtime + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: [ReadWriteOnce] + {{- with .Values.persistence.runtime.storageClassName }} + storageClassName: {{ . }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.runtime.size }} +{{- end }} diff --git a/chart/paperless-ngx/templates/service.yaml b/chart/paperless-ngx/templates/service.yaml new file mode 100644 index 0000000..ae5e4b1 --- /dev/null +++ b/chart/paperless-ngx/templates/service.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Service +metadata: + name: paperless +spec: + selector: {app: paperless} + ports: [{name: http, port: 8000, targetPort: http}] diff --git a/chart/paperless-ngx/values.schema.json b/chart/paperless-ngx/values.schema.json new file mode 100644 index 0000000..42d1f04 --- /dev/null +++ b/chart/paperless-ngx/values.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["database", "secretKey"], + "properties": { + "database": {"type": "object", "required": ["passwordSecretName"], "properties": {"passwordSecretName": {"type": "string", "minLength": 1}}}, + "secretKey": {"type": "object", "required": ["secretName"], "properties": {"secretName": {"type": "string", "minLength": 1}}}, + "persistence": {"type": "object", "properties": { + "data": {"type": "object", "properties": {"existingClaim": {"type": "string"}}}, + "runtime": {"type": "object", "properties": {"existingClaim": {"type": "string"}}} + }} + } +} diff --git a/chart/paperless-ngx/values.yaml b/chart/paperless-ngx/values.yaml new file mode 100644 index 0000000..eb719c6 --- /dev/null +++ b/chart/paperless-ngx/values.yaml @@ -0,0 +1,44 @@ +image: + repository: ghcr.io/paperless-ngx/paperless-ngx + tag: "" + pullPolicy: IfNotPresent +replicaCount: 1 +url: "" +timeZone: UTC +ocrLanguage: eng +taskWorkers: 2 +threadsPerWorker: 1 +consumerPollingInterval: 5 +database: + host: postgresql + name: paperless + user: paperless + passwordSecretName: "" + passwordSecretKey: USERDB_PASSWORD +cache: + url: redis://valkey:6379 +secretKey: + secretName: "" + secretKey: secret-key +oidc: + enabled: false + providersSecretName: "" + providersSecretKey: providers + adminGroup: Paperless Admins +persistence: + data: + existingClaim: "" + size: 20Gi + storageClassName: "" + runtime: + existingClaim: "" + size: 4Gi + storageClassName: "" +ingress: + enabled: false + className: "" + host: "" + annotations: {} +affinity: {} +tolerations: [] +resources: {} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..e0a6181 --- /dev/null +++ b/install.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu +cd "$(dirname "$0")" +values=${1:?usage: ./install.sh VALUES_FILE} +./test.sh +helm upgrade --install paperless chart/paperless-ngx --namespace paperless-ngx --create-namespace --values "$values" --wait --timeout 15m diff --git a/scripts/chart-version.sh b/scripts/chart-version.sh new file mode 100755 index 0000000..5875433 --- /dev/null +++ b/scripts/chart-version.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu +description=$(git describe --tags --long --match 'v[0-9]*' --dirty) +case "$description" in *-dirty) echo 'Refusing to package a dirty tree' >&2; exit 1 ;; esac +version=${description#v} +case "$version" in *-0-g*) version=${version%%-0-g*} ;; esac +printf '%s\n' "$version" diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..3ba7851 --- /dev/null +++ b/test.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu +cd "$(dirname "$0")" +helm lint --strict chart/paperless-ngx -f values.example.yaml +helm template paperless chart/paperless-ngx -n example -f values.example.yaml | python3 -c ' +import sys, yaml +objects=[x for x in yaml.safe_load_all(sys.stdin) if x] +one=lambda kind,name: next(x for x in objects if x["kind"]==kind and x["metadata"]["name"]==name) +d=one("Deployment","paperless") +s=one("Service","paperless") +assert d["spec"]["selector"]["matchLabels"]==s["spec"]["selector"] +c=d["spec"]["template"]["spec"]["containers"][0] +assert all(k in c for k in ("startupProbe","readinessProbe","livenessProbe")) +assert {v["name"] for v in d["spec"]["template"]["spec"]["volumes"]}=={"data","runtime"} +assert any(e["name"]=="PAPERLESS_DBPASS" and "secretKeyRef" in e["valueFrom"] for e in c["env"]) +assert not any(x["kind"]=="Secret" for x in objects) +assert one("PersistentVolumeClaim","paperless-data") and one("PersistentVolumeClaim","paperless-runtime") +' diff --git a/values.example.yaml b/values.example.yaml new file mode 100644 index 0000000..d3d2bd8 --- /dev/null +++ b/values.example.yaml @@ -0,0 +1,14 @@ +url: https://paper.example.com +database: + passwordSecretName: paperless-postgres +secretKey: + secretName: paperless-secret-key +persistence: + data: + size: 20Gi + runtime: + size: 4Gi +ingress: + enabled: true + className: traefik + host: paper.example.com