Docs/Build & operate
Deployment guide

Deployment

Run Probara with local processes, Docker Compose, or Kubernetes and Helm, with explicit guidance for databases, NATS, ingress, private locations, migrations, scaling, and current packaging limitations.

Choose a deployment workflow

WorkflowBest forWhat runs where
make start-all-localFeature development and debuggingPostgreSQL, NATS and the local AI container in Docker; locally built Go services and Next.js UI on the host.
make start-allTesting backend container images locallyPostgreSQL, NATS, local AI, and Go services in Compose; Next.js UI still runs on the host.
docker compose up -d / make upRaw backend Compose lifecycleCompose services only. There is no frontend service in the current root Compose file.
Helm chartKubernetes staging and productionFrontend, services, local AI inference, migration hook, and optionally embedded PostgreSQL/NATS in the cluster.

Prerequisites

  • Docker with Compose v2 for PostgreSQL, NATS, migrations, and container workflows.
  • GNU Make and a Bash-compatible environment for repository scripts. On Windows, use WSL or another environment that can execute the Makefile’s Bash commands.
  • Git LFS to build the local AI image from source: run git lfs install and git lfs pull --include 'ai-service/artifacts/*/head.safetensors' once after cloning. Not needed when a release image is pulled or with LOCAL_AI_DISABLED=true.
  • Go 1.26.5 for the main monorepo. The local launcher compares the installed major/minor version with the root go.mod directive.
  • Node.js LTS and npm for the frontend. scripts/start-ui.sh can use nvm when it is available.
  • Helm 3 and kubectl for Kubernetes deployment.
  • A PostgreSQL 16-compatible database and NATS 2.10 with JetStream for production when not using embedded chart dependencies.

The host agent (probara-collector) is not a separate Go module: scripts/build-collector.sh generates and cross-compiles a minimal OpenTelemetry Collector distribution from collector/manifest.yaml with a pinned builder version. Root make test covers the whole repository.

Prepare required values

Create local environment values
cp .env.example .env

# Generate stable secrets; keep them across restarts and upgrades.
openssl rand -hex 32       # ADMIN_JWT_SECRET
openssl rand -base64 32    # PROBARA_SECRETS_KEY
  • Set ADMIN_JWT_SECRET to a random value of at least 32 characters. Validation only rejects an empty value, so shipped placeholder defaults must be replaced deliberately.
  • Set PUBLIC_BASE_URL to the externally reachable API origin. For local use, http://localhost:8080 is appropriate.
  • Set PROBARA_SECRETS_KEY before storing monitor credentials, notification secrets, location credentials, or tenant AI keys; see encryption keys for format and rotation.
  • Do not set PUBLIC_NATS_URL until NATS authentication and an externally reachable TLS/WSS endpoint are ready.

Local-process stack

Start and stop
make start-all-local

# Logs
tail -f /tmp/probara-*.log
tail -f /tmp/probara-ui.log

# Matching lifecycle
make restart-all-local
make stop-all-local

The target starts PostgreSQL and NATS, bootstraps local database access, validates Go, runs migrations, builds downloadable collector binaries, builds and launches each Go service, starts the local-ai container in the background (the first image build takes several minutes; progress in /tmp/probara-local-start.log), and starts Next.js on 0.0.0.0:3000. Until the container is up, or if it fails to start, API and worker report the model unavailable; the platform is never blocked. With no local image and the head weights still a Git LFS pointer, it only tries a pull. A local build is reused as is, so rebuild it with docker compose build local-ai after an ai-service/ change. LOCAL_AI_DISABLED=true skips the container and turns the feature off.

.dev-secrets.key
Gitignored, persistent local encryption key created by the launcher.
.dev-local-ai-token
Gitignored per-checkout local AI bearer token, created on first start when LOCAL_AI_TOKEN is not exported, and passed to the container, API and worker.
/tmp/probara-*.pid and /tmp/probara-*.log
Local service process tracking and logs used by the start/stop scripts.

Docker Compose stack

Docker-backed backend with local UI
make start-all
make ps
make logs
make healthcheck
make stop-all
Raw Compose operations
docker compose up -d
docker compose ps
docker compose logs -f
docker compose down
ComponentHost portContainer behavior
Frontend3000Started on the host by Make; not a Compose service.
API HTTP, health, readiness, metrics8080All API routes, including /metrics, use the HTTP listener.
API declared metrics port9090Mapped by Compose, but the API currently does not start a separate listener there.
Scheduler metrics/health9091The scheduler’s real HTTP listener.
Scheduler declared HTTP port8081Mapped by Compose, but scheduler has no listener on HTTP_PORT.
Worker metrics/health/mesh9092Metrics listener and mesh echo.
Worker dedicated mesh HTTPNot published (8083 inside container)Starts because HTTP and metrics ports differ; Compose exposes only metrics port.
Status pages8082Public rendered pages and status HTTP routes.
Status metrics/health9093Separate operational listener.
Alerter metrics/health9094Only published alerter listener.
PostgreSQL5432Development database, user/database/password probara.
NATS4222Client connection; no local authentication or TLS.
NATS monitor8222NATS HTTP monitoring endpoint.
Local AI inference8090 on 127.0.0.1 onlyCPU inference for API and worker; /healthz, authenticated /readyz, /metrics.

Local AI is on by default. Compose pulls LOCAL_AI_IMAGE and builds it from ai-service/ only when the pull fails; that build fails on purpose unless the committed artifact is release-approved, and a failed build aborts a whole docker compose up. The Make targets (up, build, rebuild, scale-workers, start-all, start-all-local) therefore start the platform first and local-ai separately, tolerating its failure. With plain Compose and no pullable approved image, name the services you want (docker compose up -d postgres nats migrations api scheduler worker status-page alerter) or set LOCAL_AI_IMAGE to an image you already have. LOCAL_AI_DISABLED=true turns the feature off and makes the Make targets skip local-ai, but plain docker compose up still pulls or builds the service.

Compose contract gaps

  • API, scheduler, and worker all use stream check-jobs and subject check.job, so scheduled and on-demand jobs share one subject contract. If you change these values, change them on all three services together.
  • The root .env file is not passed as a service env_file; only variables explicitly declared in Compose reach containers. PROBARA_SECRETS_KEY is forwarded from the shell environment to the api, scheduler, worker, and alerter services.
  • SMTP, asynchronous notifications, public NATS authorization, scheduler mesh/purge, and many result-ingest settings are not wired.
  • Without an exported LOCAL_AI_TOKEN (shell or .env), local-ai, API and worker share a fixed development token that is public in the repository. That is tolerable only because port 8090 is bound to loopback; set your own (openssl rand -hex 32) on any shared host. make start-all-local generates a per-checkout token instead.
  • The commented private-location worker example omits required LOCATION_CREDENTIAL and should not be enabled as written.
  • The API and worker correctly share the synthetic_artifacts volume used for failure screenshots. Trace and HAR capture are configured by the schema but are not implemented by the current worker.

Install with Helm

Minimal values.prod.yaml
image:
  tag: "pin-an-immutable-release"

secrets:
  adminJwtSecret: "replace-with-at-least-32-random-characters"
  probaraSecretsKey: "base64-encoded-32-byte-key"

api:
  publicBaseURL: "https://probara.example.com"

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: probara.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: probara-tls
      hosts:
        - probara.example.com
Install or upgrade
helm lint ./helm/monitoring-platform \
  --set secrets.adminJwtSecret="$(openssl rand -hex 32)" \
  --set api.publicBaseURL="https://probara.example.com"

helm upgrade --install probara ./helm/monitoring-platform \
  --namespace probara \
  --create-namespace \
  --values values.prod.yaml

kubectl -n probara get pods,svc,ingress
kubectl -n probara get jobs

Helm values: images and infrastructure

ValueDefaultMeaning
global.imageRegistryEmptyDeclared but currently not used when rendering image names.
global.imagePullPolicyIfNotPresentGlobal backend pull policy; also wins over the frontend-specific pull policy in current templates.
image.repositoryghcr.io/yassinebenameur/probaraBackend image base repository.
image.tagEmpty: the chart’s appVersionTag for every Probara image (backend services, migrations, frontend and local AI, unless their own tag is set). Each release pushes all images under its version, so the chart’s own version always resolves; latest exists only for stable releases, never on the prerelease line. Pin for production.
image.pullSecrets[]Backend registry pull secrets.
postgresql.enabledtrueDeploy embedded PostgreSQL StatefulSet.
postgresql.externalUrlEmptyUsed when embedded PostgreSQL is disabled. Required in that mode unless an existing Secret supplies the DSN — an empty external URL now fails the render instead of starting pods with a blank DSN.
postgresql.existingSecret, .existingSecretKeyEmpty, postgres_urlRead the full DSN from a Secret you supply instead of externalUrl or the embedded credentials. Works in either mode and takes precedence over both.
postgresql.imagepostgres:16-alpineEmbedded database image.
postgresql.auth.usernameprobaraEmbedded database user.
postgresql.auth.passwordprobara-secretEmbedded password; replace it.
postgresql.auth.databaseprobaraEmbedded database name.
postgresql.auth.existingSecret, .existingSecretUserKey, .existingSecretPasswordKeyEmpty, postgresql_username, postgresql_passwordEmbedded-server credentials from a Secret you supply. Requires the DSN to be externalized too — via postgresql.existingSecret or the chart-wide secrets.existingSecret — because Helm cannot compose a DSN out of values it is not allowed to read. The render fails if neither is set.
postgresql.persistence.enabledtrueUse a PVC for database data.
postgresql.persistence.size10GiDatabase PVC request.
postgresql.persistence.storageClassEmptyCluster default storage class.
postgresql.resources100m/256Mi request; 500m/512Mi limitEmbedded database resources.
nats.enabledtrueDeploy embedded NATS StatefulSet.
nats.externalUrlEmptyUsed when embedded NATS is disabled.
nats.existingSecret, .existingSecretKeyEmpty, nats_urlRead the full broker URL — credentials included — from a Secret you supply. Required alongside nats.auth.platformPasswordExistingSecret on the embedded broker (or covered by the chart-wide secrets.existingSecret), since the chart otherwise has to compose the URL around a password it cannot read.
nats.auth.platformPasswordExistingSecret, .platformPasswordExistingSecretKeyEmpty, nats_platform_passwordPlatform-user password for the embedded broker. It reaches nats-server as a --pass argument, substituted by Kubernetes from a Secret-backed environment variable, so no password is written into the ConfigMap in either mode. It deliberately does not travel through nats.conf — nats-server re-parses an expanded $VAR as configuration, which breaks purely numeric or space-bearing passwords.
nats.auth.locationIssuerSeedExistingSecret, .locationIssuerSeedExistingSecretKeyEmpty, nats_location_auth_issuer_seedAuth-callout account seed for the API. The matching public key stays a plain ConfigMap value.
nats.imagenats:2.10-alpineEmbedded broker image.
nats.persistence.enabledtruePersist JetStream state.
nats.persistence.size1GiNATS PVC request.
nats.persistence.storageClassEmptyCluster default storage class.
nats.resources50m/64Mi request; 200m/256Mi limitEmbedded NATS resources.

Helm values: auth and service workloads

Value familyDefaultsMeaning / caveat
extraEnv, <service>.extraEnv[]Standard EnvVar entries appended to workloads: top-level extraEnv reaches every workload (including the migrations job); per-service lists (api, scheduler, worker, alerter, statusPage, frontend, migrations) reach one. worker.extraEnv also applies to location workers, and each worker.locations entry may carry its own extraEnv. Use this for any supported runtime variable without a dedicated chart value (rotation keys, SSRF policy). Top-level extraEnv never reaches the local AI pod, which holds no platform credentials; it has its own localAI.extraEnv. Notification egress (NOTIFICATION_BLOCK_PRIVATE_IPS, NOTIFICATION_ALLOWED_CIDRS) blocks private destinations by default; to let channels reach an internal receiver, set the allow-list through top-level extraEnv so the API, worker and alerter all get it.
smtp.host, .port, .useTLS, .from, .username, .password, .existingSecret, .existingSecretKey"", 465, true, "", "", "", "", passwordMail transport for email alert channels, rendered into the API, worker, and alerter together — the API needs it for the channel test endpoint, not only the alerter. Empty host disables email delivery. useTLS selects implicit TLS (pair with port 465), not STARTTLS. Prefer existingSecret over an inline password.
auth.auditRetentionDays365Audit retention days; 0 preserves records indefinitely.
auth.oidc.*Disabled; blank issuer/client; scopes openid profile email; groups claim groups; label SSO; JIT viewer/default tenantPlatform OIDC settings. Secret is secrets.oidcClientSecret. Add groups to auth.oidc.scopes when using OIDC group mappings.
secrets.existingSecretEmptyChart-wide fallback Secret backing every credential without a per-field reference. When set the chart renders no Secret of its own. See “Secrets from an external secret manager” below for the key names.
secrets.adminJwtSecretInsecure placeholder defaultShips as change-me-in-production-jwt-secret-minimum-32-chars; no template validation rejects it, so replace it with at least 32 random characters, or point secrets.adminJwtSecretExistingSecret at a Secret.
secrets.initialApiKeyInsecure placeholderStored in the chart Secret but not consumed by any bootstrap path. It does not create an API key.
secrets.probaraSecretsKeyEmptyBase64 32-byte encryption key, provided to the API, scheduler, worker, and alerter. Supplied by either the literal or an existing Secret; with neither, the key is absent and at-rest encryption stays off.
secrets.oidcClientSecretEmptyRequired when OIDC is enabled — the render now fails if neither it nor secrets.oidcClientSecretExistingSecret is set, instead of leaving the API pod unable to start on a missing Secret key.
secrets.<field>ExistingSecret, .<field>ExistingSecretKeyEmpty; canonical key namePer-field overrides for adminJwtSecret, probaraSecretsKey, and oidcClientSecret. They win over secrets.existingSecret and may point at different Secrets, so chart-managed, chart-wide, and per-field sources mix field by field.
migrations.enabledtrueCreates a post-install/post-upgrade migration hook Job.
migrations.image.repository, .tag, .resourcesDerived image; 50m/64Mi request, 200m/256Mi limitOverride migration image independently when needed.
api.enabled, .replicas, .httpPort, .metricsPort, .logLeveltrue, 2, 8080, 9090, infoAPI workload. publicBaseURL is required when enabled.
api.publicBaseURL, .publicNatsURLEmptyExternal API origin and optional external TLS/WSS NATS endpoint.
scheduler.enabled, .replicas, .metricsPort, .logLeveltrue, 1, 9090, infoScheduling and retention workload.
scheduler.checkJobStream, .checkJobSubject, .intervalSecondscheck-jobs, check.job, 5Must match worker and API. Current API subject wiring does not match.
scheduler.retentionCleanup*true, hour 2, batch 5000, max 200000Scheduled telemetry retention limits.
worker.enabled, .replicas, .metricsPort, .httpPort, .logLeveltrue, 2, 9090, 8080, infoDefault check-execution fleet and mesh echo port.
worker.checkJobStream, .checkJobSubject, .consumerName, .concurrencycheck-jobs, check.job, worker, 10Queue contract and worker parallelism.
worker.autoscaling.*Disabled; 2–10 replicas; 80% CPUCreates an HPA for the default worker deployment.
alerter.enabled, .replicas, .metricsPort, .logLeveltrue, 2, 9090, infoAlert evaluation workload. SMTP comes from the top-level smtp block; other advanced alert settings are not chart values.

Each service group also accepts a standard resources.requests and resources.limits object. Defaults are intentionally small and should be sized from observed check volume, job latency, database load, and notification throughput.

Helm values: status, frontend, ingress, and pod policy

Value familyDefaultsMeaning / caveat
statusPage.enabled, .replicas, .httpPort, .metricsPort, .logLeveltrue, 2, 8080, 9090, infoPublic status renderer.
statusPage.baseUrlEmptyPassed as STATUS_PAGE_BASE_URL, which currently has no runtime use.
statusPage.service.*ClusterIP, port 8080, null NodePortsPublic/metrics NodePorts may be fixed only when using NodePort.
frontend.enabled, .replicas, .httpPorttrue, 2, 3000Next.js deployment.
frontend.apiUrl, .statusPageUrl, .apiProxyTarget/api, empty, auto in-cluster APIPublic variables are build-time in Next.js; proxy target is runtime server-side.
frontend.image.*Dedicated frontend repository, empty tag (image.tag, then the chart’s appVersion), IfNotPresent, no secretsPin tag. Global pull policy currently masks the frontend-specific pull policy.
frontend.service.*ClusterIP, port 3000Frontend Service settings.
ingress.enabled, .className, .annotations, .hosts, .tlsDisabled; nginx; example frontend and status hostsThe current routing template gives / to frontend and /api to API; see warning below.
serviceAccount.create, .annotations, .nametrue, {}, emptyWorkload service account selection.
podAnnotationsPrometheus scrape on port 9090 at /metricsApplied to workloads; verify API metrics reachability because API serves metrics on its HTTP listener.
podSecurityContextfsGroup/runAsUser 1000; non-rootPod-level identity defaults.
securityContextNo privilege escalation; drop all capabilities; writable root filesystemContainer hardening defaults. Read-only root remains disabled.

Helm values: local AI

The chart deploys the bundled CPU inference service by default and configures only the API and the platform worker to call it. The alerter never calls it and receives only the LOCAL_AI_DISABLED and AI_TRIAGE_ENABLED kill switches; scheduler, status page and location workers never receive LOCAL_AI_*.

ValueDefaultMeaning
localAI.enabledtrueDeploy <release>-local-ai (Deployment, ClusterIP Service on 8090, NetworkPolicy). false without externalUrl renders LOCAL_AI_DISABLED=true on API, worker and alerter, which overrides every workspace setting (on the alerter it stops suspected-cause follow-ups).
localAI.externalUrlEmptyAn operator-run service speaking the same /v1/decisions protocol. Requires enabled=false and an explicit token; must be http(s)://host[:port] with no path, query or credentials. Each rule fails the render.
localAI.image.repository, .tag, .pullPolicyEmpty: <image.repository>-local-ai, image.tag (then the chart’s appVersion), global.imagePullPolicyThe image carries the model weights and runs offline. A private registry needs image.pullSecrets, which this pod also uses.
localAI.threads4; 1–4LOCAL_AI_THREADS. Keep it at or below the CPU limit; the model is CPU-validated at 4.
localAI.queueCapacity4; 1–32LOCAL_AI_QUEUE_CAPACITY: batches waiting behind the executing one before 429.
localAI.queueTimeoutSeconds60; 1–300, below timeoutSecondsLOCAL_AI_QUEUE_TIMEOUT_SECONDS: how long a waiting batch waits for the executing one before 429. A full 50-decision batch takes about 15–20 s on 4 threads. The render fails unless it is lower than localAI.timeoutSeconds, so a queued caller gets a retryable 429 before its own client gives up.
localAI.maxInferenceSeconds120; 1–600LOCAL_AI_MAX_INFERENCE_SECONDS: a batch running longer is wedged; /healthz answers 503 and the liveness probe restarts the pod.
localAI.timeoutSeconds90; 1–300LOCAL_AI_TIMEOUT_SECONDS on API and worker.
localAI.triageEnabledtrueAI_TRIAGE_ENABLED on the platform worker, the alerter and the API. false stops new alert triages (queued ones drain as skipped) and every suspected-cause follow-up, whatever the workspace settings say, and SLA reports then list no alert as Triage pending.
localAI.resources1 CPU/2Gi request; 4 CPU/4Gi limitOne process, one replica, Recreate strategy, so two copies of the model never run at once during a rollout.
localAI.podSecurityContextuid/gid 1000, non-root, RuntimeDefault seccompThe container also runs with a read-only root filesystem, no privilege escalation, all capabilities dropped, no service-account token and a 64Mi /tmp.
localAI.podAnnotations, .nodeSelector, .tolerations, .affinityEmptyNot merged with the top-level values: the top-level scrape annotation names port 9090, while this pod serves /metrics on 8090.
localAI.networkPolicy.enabled, .extraIngresstrue, []Ingress only from this release’s API and worker pods on 8090, and no egress at all, not even DNS. Add a Prometheus rule through extraIngress.
localAI.extraEnv[]The only extra environment the pod gets (the top-level extraEnv does not reach it). Every LOCAL_AI_* setting the service reads has its own value above.
secrets.localAiToken, .localAiTokenExistingSecret, .localAiTokenExistingSecretKeyEmpty, empty, local_ai_tokenBearer token shared by API, worker and the service; at least 16 characters.

The token resolves like every other chart credential: secrets.localAiTokenExistingSecret, then the chart-wide secrets.existingSecret, then the chart-managed Secret. When chart-managed and left empty, the chart derives it from secrets.adminJwtSecret (a SHA-256 over a domain-separated copy) instead of generating a random value, so helm template and GitOps renderers produce the same Secret on every render. The derived token is as private as your JWT secret: with the shipped placeholder JWT secret it is a well-known value, and rotating adminJwtSecret rotates it, after which API, worker and local-ai need a restart together.

  • The render fails, asking for secrets.localAiToken or an existing Secret, when localAI.externalUrl is set (the operator-run service must hold the same token) or when adminJwtSecret is empty or supplied by an existing Secret (the chart cannot read it to derive from).
  • API and worker reference the token as optional, so an existing Secret that predates local AI does not stop them on upgrade: they start and report local AI as not configured correctly. The local AI pod’s reference is required and it waits in CreateContainerConfigError until the key exists. The chart’s install/upgrade notes print this warning when secrets.existingSecret is set without a per-field token Secret.
  • Readiness execs the image’s Python against the authenticated /readyz, because an httpGet probe cannot read the token from a Secret; like the service and the clients, it ignores whitespace around the token. The startup probe allows five minutes for the model to load. Liveness polls /healthz every 15 seconds and restarts the pod after three failures, which happens once a batch has run past maxInferenceSeconds: a hung inference cannot be cancelled in the process, and with one replica only a restart recovers.

Secrets from an external secret manager

Every credential the chart consumes can come from a Secret you supply instead of a literal in your values file. The chart takes no dependency on any particular tool: External Secrets Operator, the Vault agent injector, sealed-secrets, SOPS, and a hand-written kubectl create secret all end at the same place — a Secret in the release namespace — and the chart only ever names it.

Each field resolves its source in a fixed order: its own <field>ExistingSecret, then the chart-wide secrets.existingSecret, then the Secret the chart renders from your literal values. Sources mix field by field, so an install can keep the JWT chart-managed while the encryption key comes from Vault and the database DSN from a cloud secret store.

Chart-wide: one Secret behind everything
secrets:
  # The chart then renders no Secret of its own.
  existingSecret: probara-platform-secrets
Per field: different stores for different credentials
postgresql:
  enabled: false
  existingSecret: rds-probara       # key: postgres_url (override with existingSecretKey)

nats:
  existingSecret: nats-platform     # key: nats_url

secrets:
  adminJwtSecret: "kept-in-the-values-file-for-this-install"
  probaraSecretsKeyExistingSecret: vault-probara
  probaraSecretsKeyExistingSecretKey: encryption_key
  oidcClientSecretExistingSecret: vault-probara

worker:
  locations:
    - name: eu-west
      locationId: "location-uuid-from-probara"
      existingSecret: probara-location-eu-west
KeyNeeded whenRead by
postgres_urlAlwaysAPI, scheduler, worker, alerter, status page, migrations job
nats_urlAlwaysAPI, scheduler, worker, alerter, status page
admin_jwt_secretAlwaysAPI
probara_secrets_keyAlways in chart-wide modeAPI, scheduler, worker, alerter. In chart-wide mode the chart assumes the Secret carries it rather than silently running with at-rest encryption off.
postgresql_username, postgresql_passwordpostgresql.enabledEmbedded PostgreSQL StatefulSet
nats_platform_passwordnats.auth.enabled on the embedded brokerEmbedded NATS server, as a --pass argument substituted by Kubernetes
nats_location_auth_issuer_seednats.auth.enabledAPI
oidc_client_secretauth.oidc.enabledAPI
local_ai_tokenlocalAI.enabled or localAI.externalUrlAPI and platform worker (optional reference), local AI pod (required)
  • Per-field <field>ExistingSecretKey values default to the canonical key names above, so a Secret laid out with those names needs no key overrides.
  • A key the chart asks for but the Secret does not carry leaves the pod in CreateContainerConfigError. That is deliberate: a missing encryption key should stop a rollout, not quietly disable encryption.
  • Helm cannot read Secrets, so it cannot assemble a URL around a value it does not have: postgresql.auth.existingSecret needs the DSN externalized too, and nats.auth.platformPasswordExistingSecret needs the broker URL externalized. Either the matching per-field reference or the chart-wide secrets.existingSecret satisfies that; setting neither fails the render with that explanation.
  • The Secret must exist before the pods start. With External Secrets Operator, that means the ExternalSecret must have synced — the migrations Job runs as a post-install/post-upgrade hook and needs postgres_url at that moment.
  • Rotating a value in the Secret does not restart the workloads; the chart sets no checksum annotation. Restart the affected Deployments yourself after a rotation.
  • Rotation keys (PROBARA_SECRETS_KEY_V2 and higher) have no dedicated value and go through the top-level extraEnv, which accepts a valueFrom.secretKeyRef entry like any Kubernetes EnvVar.

External PostgreSQL and NATS

External infrastructure values
postgresql:
  enabled: false
  externalUrl: "postgres://probara:REDACTED@postgres.example.internal:5432/probara?sslmode=require"

nats:
  enabled: false
  externalUrl: "tls://probara-platform:REDACTED@nats.example.internal:4222"

api:
  publicBaseURL: "https://probara.example.com"
  # Set only if private locations can reach this broker address.
  publicNatsURL: "tls://nats.example.com:4222"
  • Use TLS certificate verification for both PostgreSQL and NATS. Do not copy the example with sslmode=disable into production.
  • Provision JetStream storage for CHECK_JOBS, CHECK_RESULTS, alert, notification, and AI workloads.
  • Keep the platform NATS credential separate from per-location credentials and restrict broker/network access.
  • External URLs written as literals live in your values file. Point postgresql.existingSecret and nats.existingSecret at Secrets instead to keep them out of it — either way the chart reads both URLs from a Secret at runtime, never from a PodSpec.
  • Test migrations against a backup before changing application versions.

Deploy private-location workers

A private-location worker needs only an authenticated NATS endpoint, its location UUID, and its location credential. It must not receive direct PostgreSQL access. Generate deploy information in the Locations UI/API after NATS authorization is configured.

In-cluster Helm location fleet
worker:
  locations:
    - name: eu-west
      locationId: "location-uuid-from-probara"
      credential: "generated-location-credential"
      natsUrl: "tls://location-id:credential@nats.example.com:4222"
      # Or replace both with a Secret in the release namespace:
      #   existingSecret: probara-location-eu-west
      #   natsUrlKey: nats_url          # default
      #   credentialKey: credential     # default
      replicas: 1
      concurrency: "10"
      meshService:
        enabled: true
        type: LoadBalancer
        annotations:
          service.beta.kubernetes.io/aws-load-balancer-internal: "true"
  • Use a DNS-safe unique name; each entry creates a separate Deployment.
  • Set existingSecret on an entry to take natsUrl and credential from a Secret instead of the values file; the literals are then no longer required and no location credential appears in the PodSpec.
  • The chart does not expose HTTP egress policy; provide HTTP_BLOCK_PRIVATE_IPS and HTTP_ALLOWED_CIDRS through extra environment configuration, and keep any allowlist limited to networks that location is explicitly trusted to monitor.
  • Expose the mesh echo endpoint only on private inter-location networks.
  • For locations outside the Kubernetes cluster, use the generated container/deployment snippet rather than granting database access.
  • The embedded NATS Service is ClusterIP-only; an external location needs a separate TLS/WSS exposure path.

Scaling and availability

ComponentScaling guidance
APIStateless around PostgreSQL/NATS and configured for two chart replicas. Preserve shared JWT/encryption/preview secrets across every replica.
SchedulerChart defaults to one replica. Treat it as a singleton unless the scheduling/retention coordination semantics have been explicitly validated.
Default workerScale manually or enable HPA. Queue consumer semantics distribute work across replicas.
Location workerScale each location independently; every replica consumes only that location’s filtered job subject.
AlerterChart defaults to two replicas, but validate duplicate-evaluation/notification behavior under your policies and NATS mode.
Status page and frontendStateless application replicas; status cache invalidation benefits from NATS updates.
Local AIExactly one replica with one inference process; a restart makes analyses and suggestions wait or fail until it is ready again. Size throughput with localAI.threads, queueCapacity and CPU limits, not replicas.
PostgreSQL and NATSBundled dependencies are simple single StatefulSets, not production HA operators. Use managed or operator-backed systems for stronger recovery objectives.
Local worker scaling
make scale-workers N=3

Migrations, upgrades, and rollback

  1. Back up PostgreSQL and record the running application/chart/image versions.
  2. Render or lint the target chart with production values and inspect Secrets, Services, queue variables, and image tags.
  3. Run the target migrations against a disposable copy or staging database.
  4. Deploy the migration-compatible application version. The chart migration Job is a post-install/post-upgrade hook; API startup also runs shared migrations.
  5. Verify /readyz, result ingestion, scheduled and on-demand checks, alert delivery, status invalidation, and private locations.
  6. Roll back application images only when the schema remains backward-compatible. Database migrations are not automatically reversed by helm rollback.

Current Helm packaging gaps

GapOperational impact
No shared browser-artifact storageAPI replicas cannot reliably serve artifacts produced on worker filesystems.
initialApiKey is unusedThe chart Secret carries a credential that authentication never recognizes; the install notes point to the first-administrator form instead.
Public Next variables are runtime-onlyPrebuilt frontend client links may retain build-time values.
API metrics Service points to an inactive separate portAPI actually serves /metrics on its HTTP listener.
Status-root ingress routes to frontendDefault second-host example does not publish the status service at /.
global.imageRegistry and generic ConfigMap are unusedChanging them does not alter workload behavior.
Embedded NATS is ClusterIP-onlyPrivate locations outside the cluster cannot connect without an additional secure exposure.
Local AI accuracy is measured on a controlled corpus onlyThe shipped checkpoint passed its CPU release gates on synthetic scenarios whose labels come from the same rules that guard its answers, so no real-world accuracy is claimed. The -local-ai image is published only on a release tag, and only from a release-approved checkpoint. docs/local-ai-validation.md in the repository records the results and their limits.
Local AI is a single replicaNo HA for inference; analyses retry on the worker and then fail with a retry action while the pod restarts.
NetworkPolicy enforcement depends on the CNIWithout an enforcing CNI the local AI service is reachable from any pod that holds the token.

Production checklist

  • Pin immutable backend, frontend, local AI, collector, migration, PostgreSQL, and NATS versions.
  • Use managed/HA PostgreSQL and NATS or define tested backup and restore objectives for embedded state.
  • Configure HTTPS, ADMIN_COOKIE_SECURE=true, a correct PUBLIC_BASE_URL, and trusted reverse-proxy headers.
  • Use stable random JWT, encryption, local AI token, OIDC, preview, SMTP, webhook, database, and NATS secrets, sourced from your secret manager rather than written into the values file.
  • Confirm the cluster CNI enforces the local AI NetworkPolicy, or isolate the service another way.
  • Align every NATS stream, subject, consumer, and status-update subject across services.
  • Keep private-destination blocking enabled and allow only narrowly scoped CIDRs.
  • Share or externalize synthetic-browser artifact storage.
  • Provide secure TLS/WSS NATS exposure before enabling remote private locations.
  • Correct ingress routing and verify browser client URLs in the built frontend image.
  • Scrape the listeners that actually serve /metrics; alert on readiness failures, result-ingest errors, queue backlog, and migration failure.
  • Run the complete verification suite and a restore drill before launch.