Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
23 changes: 23 additions & 0 deletions charts/paperless-gpt/.helmignore
Original file line number Diff line number Diff line change
@@ -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/
20 changes: 20 additions & 0 deletions charts/paperless-gpt/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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"
65 changes: 65 additions & 0 deletions charts/paperless-gpt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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
```

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`.

## 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.
10 changes: 10 additions & 0 deletions charts/paperless-gpt/ci/ct-values.yaml
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions charts/paperless-gpt/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -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 }}
133 changes: 133 additions & 0 deletions charts/paperless-gpt/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions charts/paperless-gpt/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
16 changes: 16 additions & 0 deletions charts/paperless-gpt/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
19 changes: 19 additions & 0 deletions charts/paperless-gpt/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading