{{- $replicas := include "sure.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.sure.replicas) -}} {{- $webStartup := fromYaml (include "sure.webStartupProbe" .Values.sure.web.startupProbe) -}} {{- $workerStartup := fromYaml (include "sure.workerStartupProbe" .Values.sure.worker.startupProbe) -}} {{- $workerLiveness := fromYaml (include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe) -}} Sure {{ .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 The first account created on a fresh instance becomes the super admin — sign in yourself before handing the URL to anyone. {{- else }} ACCESS — NO INGRESS ingress.enabled is false, so this release has no Ingress and no certificate. ingress.domain is still required: it becomes APP_DOMAIN (the host in every mail link) and the WebAuthn relying party the app binds passkeys to. Reach the instance inside the cluster with kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 and open http://localhost:8080. With the Ingress off the chart also renders RAILS_ASSUME_SSL and RAILS_FORCE_SSL as "false", which is what makes that plain-HTTP session able to log in at all; mail links still point at https://{{ .Values.ingress.domain }}/. {{- end }} {{ if eq $replicas "0" }} SCALED TO ZERO {{ if .Values.scaleDown }}scaleDown: true{{ else }}sure.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so neither the web nor the worker process is running. PVC, Secrets, Service{{ if .Values.ingress.enabled }}, Ingress and the certificate stay{{ else }} stay{{ end }} in place; set it back and upgrade to bring both containers up. {{ end }} WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN The chart creates OnePasswordItem CRs; the operator turns each into a Kubernetes Secret whose keys are the FIELD NAMES of the 1Password item. A missing field does not fail the install — the release goes through and the pod then hangs in CreateContainerConfigError naming the key it could not find (kubectl -n {{ .Release.Namespace }} describe pod). secrets.appSecretRef → {{ .Values.secrets.appSecretRef }} Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, field: secret-key-base SECRET_KEY_BASE, 64 random bytes hex (openssl rand -hex 64) secrets.dbSecretRef → {{ .Values.secrets.dbSecretRef }} Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret, fields: host DB_HOST, the PostgreSQL hostname port DB_PORT, usually 5432 database POSTGRES_DB username POSTGRES_USER password POSTGRES_PASSWORD secrets.redisSecretRef → {{ .Values.secrets.redisSecretRef }} Secret {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret, field: connection-url REDIS_URL, e.g. redis://:password@host:6379/0 ROTATING SECRET_KEY_BASE DESTROYS MORE THAN SESSIONS In self-hosted mode Sure derives its Active Record encryption keys from SECRET_KEY_BASE (config/initializers/active_record_encryption.rb: three SHA-256 digests over that one value) unless ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY and its two companions are set. Every encrypted column in the database — provider tokens, OAuth credentials, MFA secrets — is therefore readable only with the SECRET_KEY_BASE it was written under. Replacing that value does not just log everyone out: it makes the existing encrypted data undecryptable, and no rotation of the item brings it back. Treat this field as permanent for the life of the database, or pin the three encryption keys explicitly through sure.web.env / sure.worker.env before the first start. CHANGING A SECRET'S CONTENT RESTARTS NOTHING Helm never sees the content of a OnePasswordItem, so it cannot hash it. The pod template carries checksum/secret-ref annotations over the rendered OnePasswordItems, so pointing a *SecretRef at a DIFFERENT item rolls the pod — but editing a value INSIDE the same item does not: the operator updates the Kubernetes Secret while both containers keep the old value in their environment. After such a change: kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs The operator's auto-restart annotation would not help here and is deliberately not set: it restarts Deployments only, and this is a StatefulSet. POSTGRES AND REDIS ARE NOT PART OF THIS CHART Both must exist and be reachable before the first start. Redis is not a cache the app can do without — it holds the Sidekiq queues, the Rails cache store and the rate limiter. What the probes do with that, per container: web liveness and startup use /up, which is Rails' boot check and touches no database — a Postgres outage therefore does not restart the pod. Readiness uses "/", which runs a User.count query, so the pod leaves the Service while the database is gone and returns on its own afterwards. worker all three probes run "sidekiqmon processes" and look for this pod's own entry, i.e. a live Sidekiq heartbeat in Redis. There is no HTTP and no dependency-free alternative: with Redis gone the worker has nothing to do, and liveness only restarts after {{ $workerLiveness.failureThreshold }} x {{ $workerLiveness.periodSeconds }}s = {{ mul $workerLiveness.failureThreshold $workerLiveness.periodSeconds }}s of missing heartbeat. A pod that never turns Ready is usually a connection problem, not a chart problem: read the container logs, not the pod events. kubectl -n {{ .Release.Namespace }} logs statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs -c {{ .Release.Name }}-{{ .Chart.Name }}-web THE FIRST START MIGRATES, AND THE STARTUP PROBE IS THE CLOCK The image entrypoint runs "./bin/rails db:prepare" — but only when the command is "./bin/rails server". That is the web container, and only the web container: the worker runs "bundle exec sidekiq" and never migrates, so the two can never race. Puma binds the port after the migration, which means the whole schema creation happens inside the startup probe's budget: web {{ $webStartup.failureThreshold }} x {{ $webStartup.periodSeconds }}s = {{ mul $webStartup.failureThreshold $webStartup.periodSeconds }}s worker {{ $workerStartup.failureThreshold }} x {{ $workerStartup.periodSeconds }}s = {{ mul $workerStartup.failureThreshold $workerStartup.periodSeconds }}s (it waits for the schema the web container creates) Raise sure.web.startupProbe.failureThreshold BEFORE a large app-version jump or a first import, not after the crash loop has started — a restart lands in the middle of a migration. Back up the database before any app-version bump: migrations are one-way, and a helm rollback returns the old image to a database the new one has already rewritten. ONE POD, ONE VOLUME, TWO CONTAINERS Uploads, imports and exports live on the ReadWriteOnce PVC {{ .Release.Name }}-{{ .Chart.Name }}-pvc, mounted at /rails/storage in both containers. That is the reason web and worker share a pod at all: a background job writes the same files the web process serves, and one RWO volume cannot be attached to two pods. {{- if gt (int $replicas) 1 }} sure.replicas is {{ $replicas }}. Expect the extra pods to stay Pending on "Multi-Attach error for volume" unless the storage class is genuinely RWX. {{- end }} Backup is two things at the same time: the database holds the accounts and one row per attachment, the PVC holds the bytes. Restoring one without the other gives an app full of broken references. Because the PVC is rendered by the chart, helm uninstall deletes it — the external database survives that, which makes an accidental uninstall look recoverable while the files are already gone. THE CHART CONFIGURES NO MAIL AND NO AI Sure sends invitations and notifications over SMTP and can run an assistant against an OpenAI-compatible endpoint. This chart sets neither: no SMTP_* variable, no OPENAI_* variable. Mails are therefore quietly never sent, and the AI features stay off — nothing in the UI says so. Both are plain environment variables, so they go into sure.web.env and, for anything a background job needs (the assistant runs in the worker), into sure.worker.env as well. The secretKeyRef form is available for the API key and the SMTP password. Note that upstream sizes AI usage at 1 GB of memory per process; the chart's computed memory limit is 3x its request. {{- if or .Values.sure.web.env .Values.sure.worker.env }} Currently set: {{ len .Values.sure.web.env }} extra variable(s) on the web container, {{ len .Values.sure.worker.env }} on the worker, rendered after the ones the chart sets itself. An entry repeating a name the chart already sets wins at install time and is silently lost on the next helm upgrade — read it back off the StatefulSet. {{- end }}