{{- $replicas := include "convertx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.convertx.replicas) -}} {{- $startup := fromYaml (include "convertx.startupProbe" .Values.convertx.startupProbe) -}} {{- $autoDelete := include "convertx.autoDeleteEveryNHours" .Values.convertx.autoDeleteEveryNHours -}} {{- $registration := include "convertx.boolString" (dict "value" .Values.convertx.accountRegistration "default" false "path" "convertx.accountRegistration") -}} {{- $unauthenticated := include "convertx.boolString" (dict "value" .Values.convertx.allowUnauthenticated "default" false "path" "convertx.allowUnauthenticated") -}} {{- $httpAllowed := include "convertx.boolString" (dict "value" .Values.convertx.httpAllowed "default" false "path" "convertx.httpAllowed") -}} ConvertX {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. {{- if .Values.ingress.enabled }} ACCESS https://{{ .Values.ingress.domain }} The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" cluster issuer. Until it has been issued the browser shows a warning; follow it with kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress {{- else }} ACCESS — NO INGRESS ingress.enabled is false, so this release has no Ingress and no certificate, and neither ingress.clusterIssuer nor ingress.domain is needed. ConvertX has no base-URL setting of its own — it builds no absolute links — so nothing outside the Ingress reads the domain in this chart. Reach the release inside the cluster with kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 and open http://localhost:8080. Note that the session cookie is marked "secure"{{ if eq $httpAllowed "true" }} — except that convertx.httpAllowed is true in this release, which clears that flag, so logging in over plain http works{{ else }}, so logging in over plain http will NOT work: the browser refuses to store the cookie. convertx.httpAllowed: true clears that flag{{ end }}. {{- end }} {{ if eq $replicas "0" }} SCALED TO ZERO {{ if .Values.scaleDown }}scaleDown: true{{ else }}convertx.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so the endpoint above does not answer until you set it back and upgrade again. PVC, Secret{{ if .Values.ingress.enabled }}, Service and Ingress stay in place, so neither the conversions nor the certificate are touched.{{ else }} and Service stay in place, so the conversions on the PVC are untouched.{{ end }} {{ end }} WHOEVER OPENS THE PAGE FIRST BECOMES THE ADMINISTRATOR A fresh ConvertX has no users, and with no users it serves a SETUP FORM to every visitor — no password, no invitation, nothing to bypass. The first account created that way owns the instance, and convertx.accountRegistration does not gate it: that value decides whether accounts TWO and onwards can be created, not the first one. So on a fresh install, register yourself before the DNS name is reachable by anyone else. There is no second chance and no "reset admin" command; the repair is deleting the users row from the SQLite database on the PVC. {{- if eq $unauthenticated "true" }} This release has convertx.allowUnauthenticated: true, so the point above is moot in the worst way — see the warning further down. {{- end }} THE 1PASSWORD ITEM MUST CARRY THE FIELD THE CHART READS secrets.jwtSecretRef points at {{ .Values.secrets.jwtSecretRef }} The 1Password operator materializes that item as the Kubernetes Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per FIELD of the item. So the field name is not free-form: this chart reads exactly one field, and it must be spelled jwt-secret a long random string — openssl rand -hex 32 An item without it does not fail the install. The release goes through and the pod then hangs in CreateContainerConfigError naming the key it could not find. Watch the materialization with kubectl -n {{ .Release.Namespace }} get onepassworditem {{ .Release.Name }}-{{ .Chart.Name }}-secret kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-secret WHY THIS VALUE IS REQUIRED RATHER THAN OPTIONAL: ConvertX signs its session cookies with JWT_SECRET, and when the variable is unset it quietly generates a random one PER PROCESS. Every restart — a node drain, an image update, an OOM kill — would then invalidate every session, with no log line and no error. Rotating the field has the same effect on purpose; after a rotation: kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs Editing the value INSIDE the item does not roll the pod by itself (Helm never sees a secret's content). Pointing secrets.jwtSecretRef at a DIFFERENT item does — the pod template carries a checksum over the rendered OnePasswordItem. TWO SWITCHES THAT OPEN THE INSTANCE UP convertx.accountRegistration {{ $registration }} convertx.allowUnauthenticated {{ $unauthenticated }} Upstream warns explicitly against enabling either on an instance reachable from the internet, and this chart puts a public DNS name in front by default. accountRegistration lets anyone with the URL create an account; allowUnauthenticated removes the login entirely, so anyone who reaches the Service can upload files and read every conversion on the volume. {{- if or (eq $registration "true") (eq $unauthenticated "true") }} *** {{ if and (eq $registration "true") (eq $unauthenticated "true") }}BOTH OF THEM ARE ON{{ else }}ONE OF THEM IS ON{{ end }} IN THIS RELEASE. *** {{- if eq $unauthenticated "true" }} convertx.allowUnauthenticated: true — there is no login. Anyone who can reach {{ if .Values.ingress.enabled }}https://{{ .Values.ingress.domain }}{{ else }}the Service{{ end }} can use it and read other people's files. {{- end }} {{- if eq $registration "true" }} convertx.accountRegistration: true — anyone with the URL can sign up. {{- end }} If this instance is reachable from the internet, that is almost certainly not what you wanted. {{- end }} RETENTION IS A VALUE, AND IT IS THE ONLY THING THAT EMPTIES THE VOLUME {{- if eq $autoDelete "0" }} convertx.autoDeleteEveryNHours is 0 — THE CLEANUP IS OFF. Uploads and converted output accumulate on the PVC for as long as the release lives and nothing reclaims them. Watch the volume, or set a non-zero number of hours. {{- else }} convertx.autoDeleteEveryNHours is {{ $autoDelete }} — ConvertX deletes the uploads and the output of any job older than {{ $autoDelete }} hours. That is a hard delete, not an archive: a conversion nobody downloaded in time is gone. Set it to 0 to switch the cleanup off, and then size storage.amount for unbounded growth. {{- end }} STORAGE {{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.storage.amount }}, {{ if .Values.storage.storageClass }}storage class "{{ .Values.storage.storageClass }}"{{ else }}on the cluster's default storage class (storage.storageClass is unset){{ end }}, mounted at /app/data. Everything this release persists is on that one volume: the SQLite database (users, jobs, file names), every uploaded file and every converted result. There is no external database and no object storage, so one snapshot of the PVC is a complete backup — and losing it loses the accounts along with the files. Because the PVC is rendered by the chart, helm uninstall deletes it and the data with it, unless the storage class reclaim policy says otherwise. {{- if gt (int $replicas) 1 }} convertx.replicas is {{ $replicas }}, and that does not work. The PVC is ReadWriteOnce, so the extra pods normally stay Pending on "Multi-Attach error for volume"; on a genuinely RWX storage class they would start and then corrupt the SQLite database two processes are now writing. Set it back to 1. {{- end }} {{- if .Values.ingress.enabled }} THE INGRESS IS THE ONLY UPLOAD CEILING ingress.maxUploadFileSize {{ .Values.ingress.maxUploadFileSize }} ConvertX enforces no size limit of its own — none, at any setting — so this annotation is the whole story. A file above it never reaches the application: the user gets the ingress controller's 413 page and nothing appears in the pod log, which makes it look like a broken upload button rather than a limit. Raising it has a second cost: the upload lands on the PVC above, so a ceiling far beyond storage.amount just moves the failure from a 413 to a full volume. {{- else }} UPLOAD CEILING — NONE FROM THIS CHART ingress.enabled is false, so the chart sets no body-size limit anywhere, and ConvertX enforces none of its own. Whatever proxy you put in front brings its ceiling with it — ingress-nginx defaults to 1m, small enough to look like a broken upload button. {{- end }} WHAT A GREEN POD PROVES All three probes are HTTP GETs against /healthcheck, which ConvertX answers with a static 200 and no authentication. It touches neither the database nor the volume, so a green pod proves the process is serving — not that a conversion works and not that the volume is writable. It is deliberately NOT "/": with the defaults of this chart "/" answers 302, to /setup while no account exists and to /login afterwards. A probe would pass on that redirect, and pass on a different one from day two onwards. The startup probe grants {{ $startup.failureThreshold }} × {{ $startup.periodSeconds }}s = {{ mul $startup.failureThreshold $startup.periodSeconds }}s. That budget is generous because ConvertX shells out to about twenty converter binaries at startup just to log their versions. THE FIRST PULL IS SLOW, AND THAT IS NORMAL The image is roughly 1.4 GiB compressed — LibreOffice, Calibre, ImageMagick, ffmpeg, Inkscape and a TeX Live installation are all inside it. On a node that has not seen it before, expect several minutes in ContainerCreating before the startup probe budget above even begins. A pod that looks stuck there is usually still pulling: kubectl -n {{ .Release.Namespace }} describe pod {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 THIS CHART BYPASSES THE IMAGE ENTRYPOINT The image runs as root and drops privileges itself; this chart starts the application directly (command: bun run dist/src/index.js) as uid 1000, with fsGroup doing what the entrypoint's chown would have done. PUID, PGID and UMASK therefore configure NOTHING here — setting them through convertx.env has no effect at all, and the uid belongs in convertx.securityContext.pod. The cost to know about: if upstream moves real work into that entrypoint, this chart skips it silently. Re-read the upstream entrypoint.sh at every app-version update. {{- if .Values.convertx.env }} EXTRA ENVIRONMENT VARIABLES ARE ACTIVE convertx.env adds {{ len .Values.convertx.env }} variable(s), rendered after the ones the chart sets itself. Two warnings that go with that block: - WEBROOT moves every route of the application, /healthcheck included, so it breaks all three probes while the app is perfectly healthy — and this chart's Ingress still maps "/". Do not set it here. - An entry repeating a name the chart already sets is additive, not a replacement. At install yours wins; on an EXISTING release Kubernetes merges env by name and the chart's own value survives instead, while helm upgrade still reports success. Read it back off the StatefulSet. {{- end }} {{- if .Values.ingress.enabled }} {{- with .Values.ingress.annotations }} EXTRA INGRESS ANNOTATIONS ARE ACTIVE {{- range $key, $value := . }} {{ $key }}: {{ $value }} {{- end }} These are merged with the cert-manager and proxy-body-size annotations the chart always sets. Annotations are interpreted by the ingress controller, so a misspelled key is silently ignored — it does not fail the render. {{- end }} {{- end }}