# Scale the whole chart down without uninstalling it: scaleDown: true renders # every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and # takes precedence over any per-workload replicas value. PVCs, Secrets and # Services stay in place; scaleDown: false brings the workloads back up. scaleDown: false # 1Password item paths (op:///). The 1Password operator turns each # OnePasswordItem into a Kubernetes Secret whose keys are the FIELD NAMES of the # item; the chart references those field names, so a renamed field is a pod in # CreateContainerConfigError, not a render error. NOTES.txt lists every field. # # Helm never sees the CONTENT of these secrets, so no checksum/config-style # annotation can cover them (README, guideline 9). The pod template does carry a # checksum/secret-ref over the rendered OnePasswordItems, so REPOINTING one of # the *SecretRef values at a different item restarts the pod; editing a value # INSIDE the same item does not, and needs a manual # kubectl rollout restart statefulset/-sure-sfs # The operator's operator.1password.io/auto-restart annotation is deliberately # NOT set: it only ever restarts Deployments, so on this StatefulSet it would # advertise a guarantee that does not exist (README, guideline 9). secrets: # SECRET_KEY_BASE (field: secret-key-base) appSecretRef: null # PostgreSQL connection (fields: host, port, database, username, password) dbSecretRef: null # Redis connection URL (field: connection-url) redisSecretRef: null sure: # Replica count for the StatefulSet; 0 is allowed and scales it down. The # chart-global scaleDown flag takes precedence. # # Both containers live in the SAME pod, so this value scales web and worker # together. That is a consequence of the single ReadWriteOnce PVC they share # at /rails/storage (Active Storage): splitting them into two workloads would # need a ReadWriteMany volume. Above 1 the extra pods normally stay Pending on # "Multi-Attach error for volume" - see NOTES.txt. replicas: 1 # Pod-level hardening, shared by both containers and by the init container. # The upstream image declares USER 1000:1000 (group and user "rails", created # in the Dockerfile), so the chart needs no deviation from the repo hardening # standard; fsGroup 1000 makes the storage PVC writable for that uid. # # readOnlyRootFilesystem: true holds, and the writable paths are emptyDirs: # /rails/tmp, /rails/log and /tmp, one set per container. /tmp needs one extra # step that is easy to mistake for decoration - a tiny init container creates # a private subdirectory in it and the containers run with TMPDIR pointing # there, because Ruby refuses a world-writable directory without a sticky bit # as its temporary directory and an emptyDir is exactly that. Without it the # app aborts during boot; the StatefulSet carries the measured evidence. securityContext: pod: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: Always # RuntimeDefault applies the container runtime's default seccomp filter, # which blocks the syscalls a normal workload never makes. It sits in the # pod context on purpose: from here it covers every container in the pod # - web and worker alike - including init containers, while a # container-level seccompProfile would have to be repeated for each one # (and silently missed on the next one added). A container that genuinely # needs a different filter can still override it under # .securityContext.container, which wins over this value. seccompProfile: type: RuntimeDefault # --------------------------------------------------------------------------- # web - Puma, the container that also runs the database migrations # --------------------------------------------------------------------------- web: securityContext: container: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: - ALL resources: # Limits are optional: memory and ephemeral-storage limits default to # limitFactor x the request, an explicitly set limit always wins, and a # CPU limit is only set when configured explicitly under "limits". # # The request follows upstream's own sizing note (docs/hosting/docker.md # for 0.7.4): steady-state memory "around 352 MB", comfortable under a # 512 MB limit, while "if you enable AI, run at least 1 GB". 512Mi request # x limitFactor 3 = 1536Mi limit, which holds both the everyday figure and # the AI recommendation without reserving the larger number on the node. # Read from upstream's documentation, not measured here. limitFactor: 3 requests: cpu: 0.1 memory: 512Mi ephemeral-storage: 10Mi # PROBES - what each one actually proves (README, guideline 12.3) # # /up is Rails' own health route (config/routes.rb: get "up" => # "rails/health#show"). READ FROM THE RAILS SOURCE, NOT MEASURED HERE: that # controller renders 200 as soon as the app has booted without raising and # touches NO database and NO Redis. It is a boot check, and the widespread # belief that it tests the database is exactly what guideline 12.3 exists to # catch - upstream's own Helm chart (charts/sure in the app repository) uses # /up for readiness, which is the mistake this chart does not copy. # # Liveness therefore uses /up: a Postgres outage must not restart the pod. livenessProbe: httpGet: path: /up port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # Readiness uses "/" instead, because that path provably reaches the # database. Read from the source of 0.7.4, not measured: every request runs # ApplicationController's authenticate_user! before_action # (app/controllers/concerns/authentication.rb); without a session cookie - # which a kubelet probe never has - it falls into self_hosted_first_login?, # which is "config.app_mode.self_hosted? && User.count.zero?". User.count is # an SQL query. With the database up the request ends in a 302 to the login # or registration page, which the kubelet counts as success (2xx-3xx); with # the database down the query raises and Rails answers 500. # # Two conditions this depends on, both of them chart-set: # - SELF_HOSTED=true. In managed mode the same code short-circuits before # User.count and "/" would redirect without touching Postgres. # - the probe must not be answered by a redirect BEFORE the controller. # Sure's production.rb defaults RAILS_FORCE_SSL to true, and a forced # redirect to https is a 301 - i.e. a "success" that proves nothing. # The chart sets RAILS_ASSUME_SSL alongside it (see the env block that # the templates always render), so requests are treated as SSL and no # redirect happens. Overriding RAILS_ASSUME_SSL to "false" while # RAILS_FORCE_SSL stays "true" turns this probe back into a lie. readinessProbe: httpGet: path: / port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # The startup probe gates liveness and readiness, so neither needs an # initialDelaySeconds. Its budget has to cover the first start, which is the # expensive one: the image entrypoint (/rails/bin/docker-entrypoint) runs # "./bin/rails db:prepare" whenever the command is "./bin/rails server", so # the web container creates and migrates the schema before Puma binds the # port. Budget 10s x 60 = 600s. Raise it before a large app-version jump; # a restart in the middle of a migration is the failure mode it prevents. startupProbe: httpGet: path: /up port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 60 # Extra environment variables, appended to the ones the chart always sets. # Values are rendered through tpl; value, secretKeyRef and configMapKeyRef # forms are supported. # # This is also where the AI features go - the chart models none of them as # values on purpose. OPENAI_ACCESS_TOKEN (secretKeyRef form), OPENAI_MODEL, # OPENAI_URI_BASE, LLM_CONTEXT_WINDOW, OPENAI_REQUEST_TIMEOUT, # AI_RESPONSE_TIMEOUT, ASSISTANT_MAX_TOOL_CALL_ITERATIONS and the SMTP_* # block are plain environment variables to this image. Set them on BOTH # containers where a background job needs them as well (the assistant runs # in the worker). # # Additive means additive: an entry repeating a name the chart already sets # reaches the pod twice at install time (the later one, yours, wins) but is # LOST on a later helm upgrade, because Kubernetes merges env by name and # the pair collapses to the chart's own value - with a green upgrade either # way (README, guideline 10). env: [] # --------------------------------------------------------------------------- # worker - Sidekiq: the same image, started with "bundle exec sidekiq" as # its args (the image entrypoint stays in place; see the StatefulSet) # --------------------------------------------------------------------------- worker: securityContext: container: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: - ALL resources: # Same sizing as the web container, for a documented reason: upstream # notes that the sample/demo-data generator runs in the WORKER and is the # one operation that deterministically exceeds 512 MB (observed flat at # ~680 MB on a 2 GB container). The computed limit of 3 x 512Mi = 1536Mi # covers that; the request stays at the everyday figure. limitFactor: 3 requests: cpu: 0.1 memory: 512Mi ephemeral-storage: 10Mi # PROBES - the worker speaks no HTTP at all, so all three are exec probes. # # The command asks Sidekiq's own monitor whether THIS process is in the # process set: sidekiqmon prints one line per registered process, each # starting with the identity "::" (sidekiq 8.0.5, # lib/sidekiq/component.rb), and in a pod the hostname is the pod name that # bash exposes as $HOSTNAME. Sidekiq keeps that registration alive with a # heartbeat whose Redis key expires after about 60s, so a process that hangs # disappears from the set while its PID still exists - which is precisely # what a process check like "pgrep sidekiq" could not tell you (and would # not tell you anything at all: Sidekiq is PID 1 here, so if it dies the # container dies with it and the check can never fail - a check a broken # system passes, README). # # READ FROM THE SIDEKIQ 8.0.5 SOURCE, NOT MEASURED: sidekiqmon is a plain # Ruby entry point that requires sidekiq/api only - it does NOT boot Rails - # and it reads REDIS_URL from the environment, which the chart sets on this # container. Cost per check is therefore one Ruby start, not an application # boot; the periods below are chosen on the assumption that this takes a few # seconds, and the PR install-test is the first place it actually runs. # # Liveness hangs on Redis, which guideline 12.3 allows only where no # dependency-free path proves anything - that is the case here (see above) - # and it is given a deliberately generous budget of 60s x 5 = 300s of # missing heartbeat, so a short Redis blip does not restart the pod. livenessProbe: exec: command: - /bin/bash - -c - bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:" periodSeconds: 60 timeoutSeconds: 15 failureThreshold: 5 # Readiness uses the same check with a tighter budget: a worker that is not # registered in Redis is not doing any work, whatever its process state. readinessProbe: exec: command: - /bin/bash - -c - bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:" periodSeconds: 30 timeoutSeconds: 15 failureThreshold: 3 # The startup probe gates the other two. Sidekiq boots the full Rails app # before it registers, and on a first install it waits for the web container # to finish db:prepare, so the budget matches the web container's: 20s x 30 # = 600s. startupProbe: exec: command: - /bin/bash - -c - bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:" periodSeconds: 20 timeoutSeconds: 15 failureThreshold: 30 # Extra environment variables for the worker, appended to the ones the chart # always sets. Same rules as sure.web.env above. env: [] # The Active Storage volume, mounted at /rails/storage in BOTH containers. # Deliberately not null-safe: an erased block yields an invalid PVC, which fails # loudly (README, guideline 12). storage: amount: 10Gi storageClass: null ingress: # Gates the whole Ingress template. Note that ingress.domain is read three # more times outside it - APP_DOMAIN (the host Action Mailer builds link URLs # from), WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS (the relying party a # passkey is bound to) - so it keeps its own required() there and stays # mandatory with the Ingress switched off; only clusterIssuer becomes # unnecessary (README, guideline 12.1). Without that guard the app would come # up with WEBAUTHN_RP_ID="" and register passkeys against the wrong relying # party - rendered cleanly, pod green. # # ingress.enabled also decides how the app is told about TLS: enabled renders # RAILS_ASSUME_SSL=true and RAILS_FORCE_SSL=true (the Ingress terminates TLS, # so Rails must trust that and may set secure cookies and HSTS), disabled # renders both as "false" so a plain-HTTP port-forward can log in at all. enabled: true clusterIssuer: null domain: null className: nginx # Extra annotations for the Ingress; merged with the cert-manager # cluster-issuer annotation the chart always sets. annotations: {}