Files
homelab/k8s/apps/paperless/paperless-ai.yaml
T
rock 4d23c983ab feat(paperless-ai): mint the LLM token with a machine key, not an init container
The init container fetched a token once at pod start with a client_credentials grant
against a web app. A web app has no such grant, so the fetch could not work; and a
token fetched once is dead within the hour while the pod runs for weeks.

llm-token-vendor runs beside the app. It signs a machine-key assertion for the
paperless-llm service account, exchanges it for a token, and rewrites
/app/data/llm_token.txt atomically at 80% of the token lifetime. The vendor image
keeps using CUSTOM_API_KEY_FILE, so it needs no change and no OIDC support.

The key is re-read on every mint, so a rotation lands without a restart: publish the
new key, the next mint uses it, retire the old one.

paperless-llm is a separate service account from poimen-llm, so revoking one caller
leaves the other working.
2026-10-27 23:48:45 -07:00

126 lines
4.8 KiB
YAML

# Secret paperless-ai-config managed via SOPS (argocd/secrets/paperless-ai-secrets.enc.yaml)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless-ai
namespace: paperless
labels:
app.kubernetes.io/name: paperless-ai
spec:
replicas: 1
# Recreate, not RollingUpdate: paperless-ai-data is a single RWO PVC. With
# RollingUpdate the new pod tries to mount the PVC while the old pod still
# holds it -> "Multi-Attach error" / stuck Init:0/1 forever, rollout wedged
# at "1 desired, 1 ready, 1 updated, 1 unavailable". Recreate terminates the
# old pod first so the PVC detaches before the new one starts. Same pattern
# as the Forgejo gitea Deployment.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: paperless-ai
template:
metadata:
labels:
app.kubernetes.io/name: paperless-ai
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
volumes:
- name: paperless-ai-data
persistentVolumeClaim:
claimName: paperless-ai-data
- name: paperless-ai-env-config
configMap:
name: paperless-ai-env
- name: llm-key
secret:
secretName: paperless-llm-key
defaultMode: 0400
# No init container. The token used to be fetched once at pod start with a
# client_credentials grant against a web app, which cannot work: a web app has
# no such grant, and even a token that was minted would be dead within the hour
# while this pod runs for weeks.
#
# llm-token-vendor runs beside the app instead. It signs a machine-key assertion,
# exchanges it for a token, and rewrites /app/data/llm_token.txt at 80% of the
# token lifetime. The vendor image keeps reading CUSTOM_API_KEY_FILE unchanged.
# The key is re-read on every mint, so a rotation lands without a restart.
containers:
- name: llm-token-vendor
image: forgejo.riotpiao.com/riotpiao-poimen/llm-token-vendor:0.1.0
imagePullPolicy: Always
args:
- --token-url=https://zitadel.riotpiao.com/oauth/v2/token
- --issuer=https://zitadel.riotpiao.com
- --user-id=397072668735570071
- --key-id=397072674272051351
- --key-file=/var/run/llm-key/key
- --scope=openid urn:zitadel:iam:org:project:id:396757796898471937:aud
- --out=/app/data/llm_token.txt
volumeMounts:
- {name: paperless-ai-data, mountPath: /app/data}
- {name: llm-key, mountPath: /var/run/llm-key, readOnly: true}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: ["ALL"]}
runAsNonRoot: true
runAsUser: 65532
seccompProfile: {type: RuntimeDefault}
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 100m, memory: 64Mi}
- name: paperless-ai
image: clusterzx/paperless-ai:latest
env:
# Paperless-ngx connection
- name: PAPERLESS_API_URL
value: "http://paperless.paperless.svc.cluster.local:8000"
- name: PAPERLESS_API_TOKEN
valueFrom:
secretKeyRef:
name: paperless-ai-config
key: PAPERLESS_API_TOKEN
- name: PAPERLESS_USERNAME
value: "admin"
# LLM API — via public gateway with JWT auth
- name: AI_PROVIDER
value: "custom"
- name: CUSTOM_BASE_URL
value: "https://api.riotpiao.com/v1"
# Token will be read from file at runtime by the application
# The init container fetches it and stores in /app/data/llm_token.txt
- name: CUSTOM_API_KEY_FILE
value: "/app/data/llm_token.txt"
- name: CUSTOM_MODEL
value: "qwen2.5:3b-instruct"
# Behavior - scan for new documents and tag them
- name: SCAN_INTERVAL
value: "60"
- name: PROCESS_PREDEFINED_DOCUMENTS
value: "no"
- name: ADD_AI_TAG
value: "yes"
- name: AI_TAG_NAME
value: "ai-processed"
- name: USE_PROMPT_TAGS
value: "yes"
- name: ADD_AI_DESCRIPTIONS
value: "yes"
volumeMounts:
- name: paperless-ai-data
mountPath: /app/data
- name: paperless-ai-env-config
mountPath: /app/data/.env
subPath: paperless-ai-config.env
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi