From 269d6f9a8fd7983ff8361787ad30e03f3fee49f2 Mon Sep 17 00:00:00 2001 From: Marco Lecheler Date: Sun, 11 Oct 2026 19:11:47 +0200 Subject: [PATCH 1/2] feat(paperless-gpt): add chart --- README.md | 1 + charts/paperless-gpt/.helmignore | 23 +++ charts/paperless-gpt/Chart.yaml | 20 +++ charts/paperless-gpt/README.md | 43 ++++++ charts/paperless-gpt/ci/ct-values.yaml | 10 ++ charts/paperless-gpt/templates/_helpers.tpl | 51 +++++++ .../paperless-gpt/templates/deployment.yaml | 133 ++++++++++++++++++ charts/paperless-gpt/templates/pvc.yaml | 21 +++ charts/paperless-gpt/templates/secret.yaml | 16 +++ charts/paperless-gpt/templates/service.yaml | 19 +++ charts/paperless-gpt/values.yaml | 96 +++++++++++++ 11 files changed, 433 insertions(+) create mode 100644 charts/paperless-gpt/.helmignore create mode 100644 charts/paperless-gpt/Chart.yaml create mode 100644 charts/paperless-gpt/README.md create mode 100644 charts/paperless-gpt/ci/ct-values.yaml create mode 100644 charts/paperless-gpt/templates/_helpers.tpl create mode 100644 charts/paperless-gpt/templates/deployment.yaml create mode 100644 charts/paperless-gpt/templates/pvc.yaml create mode 100644 charts/paperless-gpt/templates/secret.yaml create mode 100644 charts/paperless-gpt/templates/service.yaml create mode 100644 charts/paperless-gpt/values.yaml diff --git a/README.md b/README.md index a6a89cc..2296d11 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ README is mirrored to `gh-pages`. | Chart | Description | Source | |-------|-------------|--------| | [ipsec-vpn-server](charts/ipsec-vpn-server) | Deploy IPsec VPN server inside K8s with optional sealed-secrets | [taskmedia/helm](https://github.com/taskmedia/helm/tree/main/charts/ipsec-vpn-server) | +| [paperless-gpt](charts/paperless-gpt) | LLM-powered OCR and metadata assistant for paperless-ngx | [taskmedia/helm](https://github.com/taskmedia/helm/tree/main/charts/paperless-gpt) | | [paperlessngx-backup](charts/paperlessngx-backup) | Backup paperless-ngx via K8s cronjob to FTP | [taskmedia/helm](https://github.com/taskmedia/helm/tree/main/charts/paperlessngx-backup) | | [paperlessngx-ftp-bridge](charts/paperlessngx-ftp-bridge) | Embedded FTP/FTPS server that forwards scanner uploads to paperless-ngx | [taskmedia/helm](https://github.com/taskmedia/helm/tree/main/charts/paperlessngx-ftp-bridge) | | [vpn-ios-profile](charts/vpn-ios-profile) | Deploy a VPN server in K8s with a provided iOS profile | [taskmedia/helm](https://github.com/taskmedia/helm/tree/main/charts/vpn-ios-profile) | diff --git a/charts/paperless-gpt/.helmignore b/charts/paperless-gpt/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/paperless-gpt/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/paperless-gpt/Chart.yaml b/charts/paperless-gpt/Chart.yaml new file mode 100644 index 0000000..700f69b --- /dev/null +++ b/charts/paperless-gpt/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: paperless-gpt +description: A Helm chart for paperless-gpt, an LLM-powered OCR and metadata assistant for paperless-ngx +home: https://helm.task.media/paperless-gpt +keywords: + - paperless-gpt + - paperless-ngx + - ocr + - llm +maintainers: + - name: fty4 + url: https://github.com/fty4 +sources: + - https://github.com/icereed/paperless-gpt + - https://github.com/taskmedia/helm +icon: https://media.task.media/images/logo.png +type: application +version: 0.1.0 +# upstream image tag without the leading v +appVersion: "0.29.0" diff --git a/charts/paperless-gpt/README.md b/charts/paperless-gpt/README.md new file mode 100644 index 0000000..cadbadc --- /dev/null +++ b/charts/paperless-gpt/README.md @@ -0,0 +1,43 @@ +# paperless-gpt + +Helm chart for [paperless-gpt](https://github.com/icereed/paperless-gpt), an LLM-powered OCR and +metadata assistant for [paperless-ngx](https://docs.paperless-ngx.com/). It uses the official +upstream image `icereed/paperless-gpt`. + +> **paperless-gpt has no built-in authentication.** The chart creates a `ClusterIP` service only; +> put an authenticating proxy (e.g. forward-auth) in front of any ingress. + +## Usage + +```bash +helm repo add taskmedia https://helm.task.media +helm install paperless-gpt taskmedia/paperless-gpt -f values.yaml +``` + +Minimal values (OpenAI, secrets from an existing secret): + +```yaml +paperless: + baseUrl: http://paperless-ngx:8000 + apiTokenExistingSecret: + name: paperless-gpt + key: paperless-api-token + +config: + LLM_PROVIDER: openai + LLM_MODEL: gpt-4o + +# every key of this secret is injected as environment variable (e.g. OPENAI_API_KEY) +existingSecret: paperless-gpt +``` + +Any upstream [environment variable](https://github.com/icereed/paperless-gpt#configuration) can be +set via `config` (plain), `secretConfig` (stored in a chart-managed secret) or `existingSecret`. + +## Notes + +- Runs a single replica with the `Recreate` strategy: state is a local SQLite database. +- `persistence` stores `/app/prompts`, `/app/config` and `/app/db` (prompts, UI settings, history). +- Runs as non-root (UID/GID 10001); `PUID`/`PGID` are not used in that mode. +- No health endpoint is documented upstream, so probes check the TCP port. +- Use `extraVolumes`/`extraVolumeMounts` for hOCR/PDF output or Google Document AI credentials. diff --git a/charts/paperless-gpt/ci/ct-values.yaml b/charts/paperless-gpt/ci/ct-values.yaml new file mode 100644 index 0000000..deee7d7 --- /dev/null +++ b/charts/paperless-gpt/ci/ct-values.yaml @@ -0,0 +1,10 @@ +# Minimal overrides so `ct lint` and `ct install` can render the chart. +paperless: + apiToken: ct-test-token + +config: + LLM_PROVIDER: openai + LLM_MODEL: gpt-4o + +secretConfig: + OPENAI_API_KEY: ct-test-key diff --git a/charts/paperless-gpt/templates/_helpers.tpl b/charts/paperless-gpt/templates/_helpers.tpl new file mode 100644 index 0000000..d308aeb --- /dev/null +++ b/charts/paperless-gpt/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "paperlessgpt.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "paperlessgpt.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "paperlessgpt.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "paperlessgpt.labels" -}} +helm.sh/chart: {{ include "paperlessgpt.chart" . }} +{{ include "paperlessgpt.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "paperlessgpt.selectorLabels" -}} +app.kubernetes.io/name: {{ include "paperlessgpt.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/paperless-gpt/templates/deployment.yaml b/charts/paperless-gpt/templates/deployment.yaml new file mode 100644 index 0000000..27891e2 --- /dev/null +++ b/charts/paperless-gpt/templates/deployment.yaml @@ -0,0 +1,133 @@ +{{- $secretName := include "paperlessgpt.fullname" . }} +{{- $tokenSecret := .Values.paperless.apiTokenExistingSecret | default dict }} +{{- if not (or .Values.paperless.apiToken $tokenSecret.name) }} +{{- fail "paperless.apiToken or paperless.apiTokenExistingSecret.name is required" }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ include "paperlessgpt.fullname" . }}" + labels: + {{- include "paperlessgpt.labels" . | nindent 4 }} +spec: + # Hardcoded, not a .Values knob: state lives in a local SQLite database on a + # single volume, so only one instance may run, and never two during a rollout. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "paperlessgpt.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + secret-hash: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + config-hash: {{ toYaml .Values.config | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "paperlessgpt.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: paperless-gpt + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default (printf "v%s" .Chart.AppVersion) }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + {{- if .Values.existingSecret }} + envFrom: + - secretRef: + name: {{ .Values.existingSecret }} + {{- end }} + env: + - name: PAPERLESS_BASE_URL + value: {{ .Values.paperless.baseUrl | quote }} + {{- with .Values.paperless.publicUrl }} + - name: PAPERLESS_PUBLIC_URL + value: {{ . | quote }} + {{- end }} + - name: PAPERLESS_API_TOKEN + valueFrom: + secretKeyRef: + name: {{ default $secretName $tokenSecret.name }} + key: {{ default "paperless-api-token" $tokenSecret.key }} + {{- range $key, $value := .Values.config }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- range $key := keys .Values.secretConfig | sortAlpha }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $key }} + {{- end }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + # no documented health endpoint upstream: probe the listening port + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 15 + periodSeconds: 30 + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + {{- if .Values.persistence.enabled }} + - name: data + mountPath: /app/prompts + subPath: prompts + - name: data + mountPath: /app/config + subPath: config + - name: data + mountPath: /app/db + subPath: db + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if or .Values.persistence.enabled .Values.extraVolumes }} + volumes: + {{- if .Values.persistence.enabled }} + - name: data + persistentVolumeClaim: + claimName: {{ default (include "paperlessgpt.fullname" .) .Values.persistence.existingClaim }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: Always diff --git a/charts/paperless-gpt/templates/pvc.yaml b/charts/paperless-gpt/templates/pvc.yaml new file mode 100644 index 0000000..83dec49 --- /dev/null +++ b/charts/paperless-gpt/templates/pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: "{{ include "paperlessgpt.fullname" . }}" + labels: + {{- include "paperlessgpt.labels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 4 }} + {{- with .Values.persistence.storageClassName }} + storageClassName: {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/charts/paperless-gpt/templates/secret.yaml b/charts/paperless-gpt/templates/secret.yaml new file mode 100644 index 0000000..6fe0d71 --- /dev/null +++ b/charts/paperless-gpt/templates/secret.yaml @@ -0,0 +1,16 @@ +{{- if or .Values.paperless.apiToken .Values.secretConfig }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ include "paperlessgpt.fullname" . }}" + labels: + {{- include "paperlessgpt.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- with .Values.paperless.apiToken }} + paperless-api-token: {{ . | quote }} + {{- end }} + {{- range $key, $value := .Values.secretConfig }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/paperless-gpt/templates/service.yaml b/charts/paperless-gpt/templates/service.yaml new file mode 100644 index 0000000..8e39f83 --- /dev/null +++ b/charts/paperless-gpt/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ include "paperlessgpt.fullname" . }}" + labels: + {{- include "paperlessgpt.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "paperlessgpt.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/charts/paperless-gpt/values.yaml b/charts/paperless-gpt/values.yaml new file mode 100644 index 0000000..a303dd0 --- /dev/null +++ b/charts/paperless-gpt/values.yaml @@ -0,0 +1,96 @@ +# image used for paperless-gpt (official upstream image) +image: + repository: icereed/paperless-gpt + # tag defaults to v + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# paperless-ngx instance to connect to +paperless: + # url with protocol but no API endpoint + baseUrl: "http://paperless-ngx:8000" + # publicUrl is used for links in the UI (optional) + publicUrl: "" + # apiToken for paperless-ngx + apiToken: "" + # apiTokenExistingSecret is used to reference an existing secret in the same namespace + apiTokenExistingSecret: + name: "" + key: "" + +# config contains plain environment variables, see +# https://github.com/icereed/paperless-gpt#configuration +# Example: +# LLM_PROVIDER: openai +# LLM_MODEL: gpt-4o +# OCR_PROVIDER: llm +config: {} + +# secretConfig contains sensitive environment variables (e.g. OPENAI_API_KEY) +# stored in a secret created by this chart +secretConfig: {} + +# existingSecret references a secret in the same namespace whose keys are all +# injected as environment variables (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY) +existingSecret: "" + +# extraEnv allows additional env entries (e.g. valueFrom references) +extraEnv: [] + +# persistence for prompts, UI settings and history (undo, OCR run log) +# Without it all of these are lost when the pod is recreated. +persistence: + enabled: true + existingClaim: "" + storageClassName: "" + accessModes: + - ReadWriteOnce + size: 1Gi + annotations: {} + +# extraVolumes and extraVolumeMounts can be used e.g. for hOCR/PDF output or +# a Google Document AI credentials file +extraVolumes: [] +extraVolumeMounts: [] + +# paperless-gpt has no built-in authentication: do not expose the service +# without an authenticating proxy in front of it. +service: + type: ClusterIP + port: 8080 + annotations: {} + +# runs as non-root; the entrypoint then skips PUID/PGID handling +podSecurityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +resources: + requests: + cpu: 10m + memory: 64Mi + +nodeSelector: {} +tolerations: [] +affinity: {} + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} From 32784778d85617f3bf181cc93e7f52fc2a8a9d99 Mon Sep 17 00:00:00 2001 From: Marco Lecheler Date: Sun, 11 Oct 2026 19:20:30 +0200 Subject: [PATCH 2/2] docs(paperless-gpt): add ollama example --- charts/paperless-gpt/README.md | 22 ++++++++++++++++++++++ charts/paperless-gpt/values.yaml | 8 +++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/charts/paperless-gpt/README.md b/charts/paperless-gpt/README.md index cadbadc..408e93e 100644 --- a/charts/paperless-gpt/README.md +++ b/charts/paperless-gpt/README.md @@ -31,6 +31,28 @@ config: existingSecret: paperless-gpt ``` +Example with a local [Ollama](https://ollama.com/) instance (no API key needed, so no secret for the LLM): + +```yaml +paperless: + baseUrl: http://paperless-ngx:8000 + apiTokenExistingSecret: + name: paperless-gpt + key: paperless-api-token + +config: + # metadata suggestions (title, tags, correspondent, ...) + LLM_PROVIDER: ollama + LLM_MODEL: qwen3:8b + OLLAMA_HOST: http://ollama.ollama.svc.cluster.local:11434 + OLLAMA_CONTEXT_LENGTH: "8192" + TOKEN_LIMIT: "1000" # recommended for smaller models + # vision based OCR + OCR_PROVIDER: llm + VISION_LLM_PROVIDER: ollama + VISION_LLM_MODEL: minicpm-v +``` + Any upstream [environment variable](https://github.com/icereed/paperless-gpt#configuration) can be set via `config` (plain), `secretConfig` (stored in a chart-managed secret) or `existingSecret`. diff --git a/charts/paperless-gpt/values.yaml b/charts/paperless-gpt/values.yaml index a303dd0..ad8c6e4 100644 --- a/charts/paperless-gpt/values.yaml +++ b/charts/paperless-gpt/values.yaml @@ -24,10 +24,16 @@ paperless: # config contains plain environment variables, see # https://github.com/icereed/paperless-gpt#configuration -# Example: +# Example (OpenAI): # LLM_PROVIDER: openai # LLM_MODEL: gpt-4o +# Example (Ollama): +# LLM_PROVIDER: ollama +# LLM_MODEL: qwen3:8b +# OLLAMA_HOST: http://ollama:11434 # OCR_PROVIDER: llm +# VISION_LLM_PROVIDER: ollama +# VISION_LLM_MODEL: minicpm-v config: {} # secretConfig contains sensitive environment variables (e.g. OPENAI_API_KEY)