Saltar al contenido principal

Self-Hosted Installation

Chart version

Examples on this page assume chart 1.2.32 or later and use the platform's canonical environment variable names (OBJECT_STORAGE_*, EMAIL_*, AI_<CHANNEL>_*, <PROVIDER>_OAUTH_*, REGISTRY_*, TEMPORAL_ENDPOINT).

Plexicus can be deployed on any Kubernetes cluster using the official Helm chart, published to a private Google Artifact Registry (GAR). This guide walks you through every step — from requesting access credentials to a running installation.

Prerequisites

Where you run these commands

Every command in this guide runs from your operator workstation — the machine that already has kubectl and helm configured to talk to the target Kubernetes cluster (laptop, jump host, or dedicated CI runner). No commands run on the cluster nodes themselves.

Pick a working directory and stay there for the whole install. The chart artifact, your values overlay, and the GAR service-account key all land in the same place:

mkdir -p ~/plexicus-deploy
cd ~/plexicus-deploy

The rest of this guide assumes the current directory is ~/plexicus-deploy. When a command refers to sa-key.json, my-values.yaml, or plexicus/, it means files inside that directory.

CLI tooling on the operator workstation

RequirementMinimum versionNotes
Kubernetes (cluster)v1.25Any CNCF-conformant distribution
Helmv3.8OCI registry support is required
kubectlMust be configured to target your cluster (kubectl config current-context should show the right context)
gitany recentRequired only for the ArgoCD / GitOps path
opensslany recentUsed to generate random secret values
Ingress controller (cluster)Traefik is the default; nginx also works
cert-manager (cluster)v1.xRecommended for automatic TLS provisioning. Note: installing cert-manager creates the controller and CRDs only — it does not create any ClusterIssuer or Issuer resource. You must create the ClusterIssuer yourself after the cert-manager pods are ready (see step 6). A missing ClusterIssuer causes certificates to stay in Pending with the message ClusterIssuer "letsencrypt-prod" not found.
tip

Run helm version to confirm your Helm version. If it is below 3.8, upgrade it before continuing — earlier versions cannot pull OCI charts.

Public DNS, before you start

The chart provisions two externally-reachable Ingress resources: plexicus.<your-domain> (the SPA) and api.plexicus.<your-domain> (the FastAPI backend). Configure both A records to point at your ingress controller's external IP before step 4 (cert-manager) — otherwise Let's Encrypt's HTTP-01 challenge will fail and your TLS certificates will never be issued.


1. Request Registry Access

The Plexicus Helm chart and all container images are hosted in a private GAR repository. You need a Google Cloud service account key to pull them.

Send an email to engineering@plexicus.ai with the following information:

  • Your organization name
  • The target environment (e.g., staging, production)
  • The Kubernetes provider you are using (e.g., GKE, EKS, AKS, on-premises)

You will receive a JSON key file (sa-key.json). Save it to your working directory (~/plexicus-deploy/sa-key.json) — every subsequent command in this guide assumes that path. This file authenticates both Helm (to pull the chart) and Kubernetes (to pull the container images at runtime).

aviso

sa-key.json is equivalent to a password. Never commit it to version control, share it over unencrypted channels, or store it in plain text on shared systems. Add sa-key.json to your shell's ~/.gitignore_global if you ever plan to git init inside this directory.


2. Authenticate Helm Against the Registry

Use the service account key to log Helm in to the OCI registry:

cat sa-key.json | helm registry login \
europe-west3-docker.pkg.dev \
--username _json_key \
--password-stdin

A successful login prints Login Succeeded. You only need to do this once per machine.


3. Create the Kubernetes Namespace

kubectl create namespace plexicus

4. Create the Image Pull Secret

Kubernetes needs the same GAR credentials to pull container images at runtime. The chart expects this secret to be named gar-secret:

kubectl create secret docker-registry gar-secret \
--docker-server=europe-west3-docker.pkg.dev \
--docker-username=_json_key \
--docker-password="$(cat sa-key.json)" \
--namespace plexicus
nota

If you rotate the service account key later, re-create this secret with the new key and restart the affected deployments.


5. Create Application Secrets

Plexicus follows a GitOps-safe secrets pattern: sensitive values (passwords, API keys, OAuth secrets) are never stored in values.yaml. Instead, each service reads them from a dedicated Kubernetes Secret at runtime via existingSecret.

You must create all secrets before installing the chart.

aviso

Use the same password value for a given dependency (e.g. DATABASE_PASSWORD) across every service that shares it. Mismatches will cause connection errors at runtime.

Key catalog

Nine Secrets are required for a standard installation — eight wired via existingSecret for service environment variables, plus one license Secret mounted as files:

Secret nameRequired keys
plexicus-fastapiDATABASE_PASSWORD, REDIS_PASSWORD, SECRET_KEY, PLEXALYZER_SECRET_KEY, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY, AI_REMEDIATION_API_KEY, AI_VALIDATION_API_KEY, WORKER_CONTROL_PLANE_SECRET (shared value — must be identical in the fastapi and worker Secrets), plus optional GITHUB_APP_PRIVATE_KEY, GITHUB_OAUTH_CLIENT_SECRET, GITLAB_OAUTH_CLIENT_SECRET, BITBUCKET_OAUTH_CLIENT_SECRET, EMAIL_PASSWORD, STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, BREAK_GLASS_SECRET_KEY, SSO_RELAY_STATE_SECRET, SSO_ENCRYPTION_KEY (only the integrations you use; SSO keys required only when SAML/OIDC SSO is enabled)
plexicus-workerDATABASE_PASSWORD, REDIS_PASSWORD, SECRET_KEY, PLEXALYZER_SECRET_KEY, GITHUB_APP_PRIVATE_KEY, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY, PLEXALYZER_TOKEN, AI_REMEDIATION_API_KEY, AI_VALIDATION_API_KEY, PLEXICUS_AI_SCAN_API_KEY, PLEXICUS_AI_SCAN_EMBEDDING_API_KEY, WORKER_CONTROL_PLANE_SECRET (shared value — must be identical in the fastapi and worker Secrets; without it the worker refuses to start), EXPORTER_INTERNAL_TOKEN (shared value — must be identical in the worker and exporter Secrets; only when exporter.enabled: true), EMAIL_PASSWORD
plexicus-frontendNUXT_SECRET_KEY; optional OAuth client secrets: NUXT_GITHUB_SECRET_KEY, NUXT_GITLAB_SECRET_KEY, NUXT_BITBUCKET_CLOUD_SECRET, NUXT_GOOGLE_CLIENT_SECRET; NUXT_STRIPE_KEY if billing is enabled; NUXT_TURNSTILE_SECRET_KEY for Cloudflare Turnstile in production (the NUXT_PILOTING_* keys required by pre-1.2.20 charts are gone — chart 1.2.20 no longer reads them)
plexicus-analysis-schedulerDATABASE_PASSWORD
plexicus-codex-remediumDATABASE_PASSWORD, REDIS_PASSWORD
plexicus-exporterDATABASE_PASSWORD, AI_ENRICHMENT_API_KEY, EXPORTER_INTERNAL_TOKEN (shared value — must be identical in the worker and exporter Secrets; the exporter refuses to start without it)
plexicus-plexalyzer-codePLEXALYZER_SECRET_KEY; optional: NVD_API_KEY (free API key from nvd.nist.gov — omitting it causes vulnerability enrichment to run at the unauthenticated rate limit: 10 requests/30 s instead of 50 requests/30 s; enrichment works but is slower)
plexicus-plexalyzer-provPLEXALYZER_SECRET_KEY
plexicus-licenselicense.jwt (required); license-key.pem (optional — include if Plexicus provided it alongside license.jwt). Both keys are mounted as files at /etc/plexicus/ by the fastapi and worker pods.
AI env vars — canonical names

The platform's AI Secret key names are AI_REMEDIATION_API_KEY (the remediation channel), AI_VALIDATION_API_KEY (the validation channel), and AI_ENRICHMENT_API_KEY (the enrichment/exporter channel). Each channel is paired with a non-secret env var AI_{REMEDIATION,VALIDATION,ENRICHMENT}_PROVIDER that selects the provider: deepinfra (default), deepseek, openai, or azure.

Plexicus provides the AI key by default — BYOAI is the override

You do not need your own AI provider account to run Plexicus. Plexicus supplies a managed AI key with your deployment (delivered alongside your registry credentials and license.jwt), and that is what the on-premises deployments we operate today actually run on. Use it as the value for AI_REMEDIATION_API_KEY, AI_VALIDATION_API_KEY and AI_ENRICHMENT_API_KEY.

Bring your own AI (BYOAI) is fully supported and is the right choice when you need the traffic billed to your own account, pinned to a specific region, or routed to a provider you already have a data-processing agreement with. To switch, replace the three Secret values with your own key and set the matching non-secret AI_*_PROVIDER / AI_*_BASE_URL / AI_*_MODEL variables for that provider (see the next admonition). Nothing else in the chart changes.

Never paste an AI key into my-values.yaml — keys belong in the Secrets above, which is why the chart reads them through existingSecret.

The AI channels need their provider, endpoint and model too — not just the key

The key alone is not enough. Since the worker resolves a non-BYOAI tenant's AI configuration from environment variables at runtime ("DB override, env default"), each channel needs its three non-secret companions set in worker.envs as well as the API key in the Secret. The chart does not template them for the worker, and without them the worker falls back to api.openai.com with a DeepInfra key and every call fails with a 401 (typically surfacing as a repository-description or validation step failing "during None inference").

The values below are what our production on-premises deployment runs:

worker:
envs:
AI_VALIDATION_PROVIDER: "deepinfra"
AI_VALIDATION_BASE_URL: "https://api.deepinfra.com/v1/openai"
AI_VALIDATION_MODEL: "deepseek-ai/DeepSeek-V4-Flash"
AI_REMEDIATION_PROVIDER: "deepinfra"
AI_REMEDIATION_BASE_URL: "https://api.deepinfra.com/v1/openai"
AI_REMEDIATION_MODEL: "deepseek-ai/DeepSeek-V4-Flash"

If you bring your own key, change all three values per channel together — a _PROVIDER label that does not match the endpoint the key belongs to sends the key to the wrong provider's API shape, and the platform's AI connection test 401s even though scanning itself works.

AI SAST (SocratiCode) scan credentials — a separate, mandatory credential surface

Since chart 1.2.27, SocratiCode guided-exploration indexing is a hard dependency of every AI SAST scan — there is no opt-out, and an unindexed repo fails the scan instead of falling back. This requires two things in the plexicus-worker Secret, distinct from the AI_VALIDATION_API_KEY/AI_REMEDIATION_API_KEY channels above:

  • PLEXICUS_AI_SCAN_API_KEY — the chat LLM the scan engine itself calls while exploring the repo. Paired with non-secret Helm values worker.envs.PLEXICUS_AI_SCAN_BASE_URL / PLEXICUS_AI_SCAN_MODEL (both empty by default in the packaged chart — an install that leaves them unset fails every scan with "PLEXICUS_AI_SCAN_API_KEY not set").
  • PLEXICUS_AI_SCAN_EMBEDDING_API_KEY — a separate key for the embedding endpoint that indexes the repo, model BAAI/bge-m3, 1024 dimensions. Paired with the non-secret value worker.envs.PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL (also no packaged-chart default — it must be set explicitly or indexing has no endpoint to call). EMBEDDING_MODEL/EMBEDDING_DIMENSIONS already default to BAAI/bge-m3/1024 in the packaged chart; only override them if you run a different embedding deployment (dimensions MUST match what your model actually outputs, or the vector index is silently corrupted instead of erroring).

DeepSeek's API serves no embedding model. If you use DeepSeek for your AI_VALIDATION_API_KEY/AI_REMEDIATION_API_KEY channel or for PLEXICUS_AI_SCAN_API_KEY, you cannot point PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL at DeepSeek — use a DeepInfra account instead (https://api.deepinfra.com/v1/openai, which does serve BAAI/bge-m3) with its own PLEXICUS_AI_SCAN_EMBEDDING_API_KEY. With DeepInfra as the chat provider (the platform default, and what the Evaluator Installation guide defaults to) one key serves both channels. The chat key (PLEXICUS_AI_SCAN_API_KEY) can stay on DeepInfra, DeepSeek, or any OpenAI-compatible provider — only the embedding endpoint is constrained to a provider that actually serves the model.

PLEXICUS_GATEWAY_BASE_URL and PLEXICUS_GATEWAY_IDENTITY_SECRET are a package deal

The chart ships an optional AI Gateway (per-call metering proxy, ai-gateway.enabled: false by default). If you set PLEXICUS_GATEWAY_BASE_URL (routes worker/fastapi AI calls through it) you must also set PLEXICUS_GATEWAY_IDENTITY_SECRET in the plexicus-worker Secret — it is the HMAC key the worker signs into the identity token AI SAST scan and Strix pods carry instead of a provider key. Set the base URL without the identity secret and every AI SAST/Strix scan fails loudly instead of quietly running unmetered. If you are not using the AI Gateway, leave PLEXICUS_GATEWAY_BASE_URL unset — that is the deliberate off switch, and the identity secret is then unread.

The Secret key names above are the chart's existingSecret contract for chart 1.2.32. The chart uses envFrom: secretRef with no key renaming, so the key names in the Secret are the variable names the application receives at runtime — every canonical name shown in this guide must appear as the literal Secret key.

Ninth service — AI Pentest

A ninth service, AI Pentest, ships with a complete implementation but is disabled by default (chart key strix.enabled: false) and is not yet officially enabled for self-hosted deployments. When enabled, it requires strix.existingSecret pointing to a Secret containing TOOL_SERVER_TOKEN and an LLM API key. No AI Pentest secret is needed during a standard installation.

Re-use the same value for shared keys

DATABASE_PASSWORD, REDIS_PASSWORD, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY, PLEXALYZER_SECRET_KEY, PLEXALYZER_TOKEN, and SECRET_KEY MUST be identical across every Secret that lists them. They are the same credential consumed by different services. Mismatches cause MongoDB Authentication failed, WRONGPASS from Redis, and Plexalyzer worker handshake failures at runtime.

WORKER_CONTROL_PLANE_SECRET is a shared value — it must be identical in the fastapi and worker Secrets. It is not optional: the chart sets ENVIRONMENT=production, which engages the worker's mandatory-token startup guard, so a worker pod without this key refuses to start and CrashLoops before it does any work. fastapi presents the same value as X-Shared-Secret when it calls the worker's control plane, so a mismatch is just as bad as an absence — the worker boots and then rejects every control-plane call from fastapi.

EXPORTER_INTERNAL_TOKEN follows the identical pattern and is a shared value — it must be identical in the worker and exporter Secrets. An exporter pod without it fails startup by design; a worker without it gets silent 401s on its daily enrichment refresh. This one applies only when exporter.enabled: true. Do not set it through exporter.envs — the chart's env guard rejects TOKEN-named plaintext envs, so it must arrive through the Secret.

AI_REMEDIATION_API_KEY and AI_VALIDATION_API_KEY must also be identical across the plexicus-fastapi and plexicus-worker Secrets — both services use them for their respective AI channels. By default this is the key Plexicus provides with your deployment. If you bring your own, it may be a DeepInfra, DeepSeek, OpenAI, or Azure OpenAI key; set the matching AI_*_PROVIDER, AI_*_BASE_URL and AI_*_MODEL non-secret variables in your Helm values accordingly.

The licence Secret is a hard prerequisite

plexicus-license must exist before helm install. It is not optional and there is no unlicensed mode: fastapi and worker both mount it as a volume with optional: false, so without it those two pods sit in ContainerCreating indefinitely with a FailedMount event for plexicus-license, produce no logs, and never become ready. Create it first:

kubectl -n plexicus create secret generic plexicus-license \
--from-file=license.jwt=/path/to/license.jwt

Plexicus provides your license.jwt (and optionally license-key.pem) together with your registry credentials when you receive evaluation or production access. If you have not yet received a license file, email engineering@plexicus.ai — do not start the install without it.

Troubleshooting license failures

The fastapi and worker pods read the license from two paths, controlled by PLEXICUS_LICENSE_PATH (required) and PLEXICUS_LICENSE_PRIVATE_KEY_PATH (optional — only needed if Plexicus provided a license-key.pem). Both env vars point at the files mounted from the plexicus-license Secret above. License problems surface as one of two distinct failure modes:

  • Pods stuck in ContainerCreating, with a FailedMount event naming plexicus-license — the Secret does not exist. Both fastapi and worker mount it as a volume with optional: false, so the kubelet refuses to start the container at all rather than starting it without the file; the pod never reaches a running state and there are no container logs to read. kubectl -n plexicus describe pod <pod> shows an event like MountVolume.SetUp failed for volume "plexicus-license" : secret "plexicus-license" not found. Fix: create the Secret as shown above — the pods recover on their own once it exists, no rollout needed.
  • CrashLoopBackOff on worker only, with WORKER_CONTROL_PLANE_SECRET must be set outside the dev environment — not a licence problem. The chart sets ENVIRONMENT=production, which makes that key mandatory at worker startup. Add it to both the plexicus-fastapi and plexicus-worker Secrets with the same value and restart the worker. The equivalent on exporter is EXPORTER_INTERNAL_TOKEN, shared between the worker and exporter Secrets.
  • CrashLoopBackOff on fastapi/worker with a clean container start — the Secret exists and mounts correctly, but the license itself is invalid or expired. Both services call SystemExit at startup when license validation fails, which Kubernetes reports as a crash loop rather than a config error. Check kubectl -n plexicus logs deploy/fastapi (or deploy/worker) for the validation error, then request a renewed license.jwt from engineering@plexicus.ai.

Bootstrap secrets for all services

The recipe below sets a handful of shell variables once, then creates every Secret. Run it from ~/plexicus-deploy after step 4. Replace each <...> placeholder.

Passwords must be alphanumeric

When you choose DB_PASS, REDIS_PASS, and MINIO_PASS, use only letters and digits (e.g. openssl rand -hex 24). pymongo refuses to URL-encode special characters (@, :, /, ?, #, %, !, $) in connection URIs and will fail with Username and password must be escaped according to RFC 3986. Redis and MinIO have similar quoting traps in their clients.

# === Shared values (set once, used across multiple Secrets) ===
DB_PASS='<choose-a-strong-mongo-root-password>'
REDIS_PASS='<choose-a-redis-password>'
MINIO_USER='minioadmin' # object-storage access key id
MINIO_PASS='<choose-a-minio-password>' # object-storage secret access key
SECRET_KEY=$(openssl rand -hex 32) # Plexicus signing key
PLEXALYZER_SECRET=$(openssl rand -hex 32) # Plexalyzer worker handshake
NUXT_SECRET=$(openssl rand -hex 32) # Nuxt session secret
# AI key: use the one Plexicus provided with your deployment, or your own
# (BYOAI) if you want the traffic billed to your account.
AI_API_KEY='<the AI key Plexicus provided, or your own deepinfra/deepseek/openai/azure key>'
AI_SCAN_KEY='<chat LLM key for the AI SAST scan engine — can reuse AI_API_KEY>'
AI_SCAN_EMBEDDING_KEY='<DeepInfra API key for embeddings — DeepSeek serves no embedding model>'

# === Optional integration secrets — leave empty if not used ===
GH_OAUTH_SECRET="" # GitHub OAuth client_secret (App or OAuth App)
GL_OAUTH_SECRET="" # GitLab OAuth client_secret
BB_OAUTH_SECRET="" # Bitbucket OAuth client_secret
EMAIL_PASS="" # SMTP password for transactional email

# === SSO / SAML / OIDC secrets — required only when SSO is enabled ===
# Generate strong random values; safe to populate even if SSO is off
# (they're only consumed when SAML/OIDC handlers are invoked).
BREAK_GLASS=$(openssl rand -hex 32) # Emergency-admin bypass key
SSO_RELAY=$(openssl rand -hex 32) # HMAC for SAML RelayState signing
SSO_ENCRYPT=$(openssl rand -hex 32) # AES-256 key for OIDC client_secret encryption at rest

# === Internal service-to-service tokens — REQUIRED, and each is a SHARED value ===
# Generate each ONCE and use the same value in every Secret listed below. These
# are not optional: the chart sets ENVIRONMENT=production, which engages the
# mandatory-token startup guards in the worker and the exporter.
WORKER_CP_SECRET=$(openssl rand -hex 32) # fastapi <-> worker control plane
EXPORTER_TOKEN=$(openssl rand -hex 32) # worker <-> exporter internal API

# === Secrets for all currently-enabled Plexicus services (AI Pentest disabled by default; no secret needed until enabled) ===
kubectl -n plexicus create secret generic plexicus-fastapi \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=REDIS_PASSWORD="$REDIS_PASS" \
--from-literal=SECRET_KEY="$SECRET_KEY" \
--from-literal=PLEXALYZER_SECRET_KEY="$PLEXALYZER_SECRET" \
--from-literal=OBJECT_STORAGE_ACCESS_KEY="$MINIO_USER" \
--from-literal=OBJECT_STORAGE_SECRET_KEY="$MINIO_PASS" \
--from-literal=AI_REMEDIATION_API_KEY="$AI_API_KEY" \
--from-literal=AI_VALIDATION_API_KEY="$AI_API_KEY" \
--from-literal=GITHUB_APP_PRIVATE_KEY="" \
--from-literal=GITHUB_OAUTH_CLIENT_SECRET="$GH_OAUTH_SECRET" \
--from-literal=GITLAB_OAUTH_CLIENT_SECRET="$GL_OAUTH_SECRET" \
--from-literal=BITBUCKET_OAUTH_CLIENT_SECRET="$BB_OAUTH_SECRET" \
--from-literal=EMAIL_PASSWORD="$EMAIL_PASS" \
--from-literal=WORKER_CONTROL_PLANE_SECRET="$WORKER_CP_SECRET" \
--from-literal=BREAK_GLASS_SECRET_KEY="$BREAK_GLASS" \
--from-literal=SSO_RELAY_STATE_SECRET="$SSO_RELAY" \
--from-literal=SSO_ENCRYPTION_KEY="$SSO_ENCRYPT"

kubectl -n plexicus create secret generic plexicus-worker \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=REDIS_PASSWORD="$REDIS_PASS" \
--from-literal=SECRET_KEY="$SECRET_KEY" \
--from-literal=PLEXALYZER_SECRET_KEY="$PLEXALYZER_SECRET" \
--from-literal=GITHUB_APP_PRIVATE_KEY="" \
--from-literal=OBJECT_STORAGE_ACCESS_KEY="$MINIO_USER" \
--from-literal=OBJECT_STORAGE_SECRET_KEY="$MINIO_PASS" \
--from-literal=PLEXALYZER_TOKEN="$PLEXALYZER_SECRET" \
--from-literal=AI_REMEDIATION_API_KEY="$AI_API_KEY" \
--from-literal=AI_VALIDATION_API_KEY="$AI_API_KEY" \
--from-literal=PLEXICUS_AI_SCAN_API_KEY="$AI_SCAN_KEY" \
--from-literal=PLEXICUS_AI_SCAN_EMBEDDING_API_KEY="$AI_SCAN_EMBEDDING_KEY" \
--from-literal=WORKER_CONTROL_PLANE_SECRET="$WORKER_CP_SECRET" \
--from-literal=EXPORTER_INTERNAL_TOKEN="$EXPORTER_TOKEN" \
--from-literal=EMAIL_PASSWORD="$EMAIL_PASS"

kubectl -n plexicus create secret generic plexicus-frontend \
--from-literal=NUXT_SECRET_KEY="$NUXT_SECRET"

kubectl -n plexicus create secret generic plexicus-analysis-scheduler \
--from-literal=DATABASE_PASSWORD="$DB_PASS"

kubectl -n plexicus create secret generic plexicus-codex-remedium \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=REDIS_PASSWORD="$REDIS_PASS"

kubectl -n plexicus create secret generic plexicus-exporter \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=AI_ENRICHMENT_API_KEY="$AI_API_KEY" \
--from-literal=EXPORTER_INTERNAL_TOKEN="$EXPORTER_TOKEN"

for s in plexalyzer-code plexalyzer-prov; do
kubectl -n plexicus create secret generic plexicus-$s \
--from-literal=PLEXALYZER_SECRET_KEY="$PLEXALYZER_SECRET"
done

# License secret — Plexicus provides license.jwt (and optionally license-key.pem)
# alongside your registry credentials. Adjust the paths below to where you saved
# the files. The fastapi and worker pods mount this Secret at /etc/plexicus/ with
# optional: false; if it is missing those pods sit in ContainerCreating forever
# with a FailedMount event for plexicus-license, and write no logs at all.
kubectl -n plexicus create secret generic plexicus-license \
--from-file=license.jwt=/path/to/license.jwt
# If Plexicus also provided license-key.pem, add the following line to the command above:
# --from-file=license-key.pem=/path/to/license-key.pem

# Verify
kubectl -n plexicus get secrets | grep ^plexicus-
# Expected: 9 plexicus-* secrets (8 service + 1 license)

If you operate a real GitHub App (preferred over plain OAuth for production), replace the empty GITHUB_APP_PRIVATE_KEY="" with GITHUB_APP_PRIVATE_KEY="$(cat /path/to/gh-app.pem)" in both plexicus-fastapi and plexicus-worker.


6. Install Infrastructure Prerequisites

Cluster sizing

Match your node sizing to the expected workload before installing:

ProfilevCPURAMNotes
Evaluation / light single-tenant48 GBMinimum that fits the platform and all bundled infra subcharts
Small production baseline816 GBRecommended starting point
High-throughput (many concurrent scans)1632 GB+Scale further with scan concurrency
Resource requests and scheduling

The chart ships default resources.requests for the services that have them (fastapi, worker, plexalyzer-*). Apply a LimitRange or equivalent to your cluster so that pods without explicit requests receive non-zero QoS. Without request values set, the Kubernetes scheduler cannot make correct placement decisions — pods land in BestEffort QoS and are the first evicted under memory pressure. See global.limitRange in the values reference for the chart's built-in defaults.

Object storage — any S3-compatible service

Plexicus talks to object storage through a generic S3 client, so any S3-compatible endpoint is the recommended production choice — AWS S3, Cloudflare R2, Ceph RGW, or your cloud provider's hosted object storage. The bundled MinIO subchart exists only as a zero-dependency default for evaluation installs; it is not a production recommendation.

# In your values overlay:
minio:
enabled: false # disable the bundled subchart

global:
required:
minio:
service: "<your-s3-host>" # hostname ONLY — no https://, no trailing port
buckets: "<your-bucket-name>"
rootUser: "<access-key-id>" # S3 access key ID, supplied via existingSecret
rootPassword: "<secret-access-key>" # S3 secret access key, via existingSecret
service must be a bare hostname

global.required.minio.service expects the hostname (and optional :port) only — no https:// prefix, no trailing slash. Adding a scheme breaks S3 connectivity without an obvious error message. Correct: s3.us-east-1.amazonaws.com. Wrong: https://s3.us-east-1.amazonaws.com.

The rootUser and rootPassword values are read by the plexicus-fastapi and plexicus-worker Secrets, as OBJECT_STORAGE_ACCESS_KEY / OBJECT_STORAGE_SECRET_KEY (the minio.* naming predates generic S3 support and stayed to avoid a breaking rename — it works the same for any S3-compatible provider). If you use an existing secret rather than literal values, set fastapi.existingSecret and worker.existingSecret to a Secret that contains those keys.

When minio.enabled: false you can skip the MinIO Helm release in the steps below — install only MongoDB, Redis, PostgreSQL, and Temporal.

StorageClass for persistent data

Use a StorageClass with reclaimPolicy: Retain for production

Set global.storageClass in your values overlay to a StorageClass that has reclaimPolicy: Retain for your MongoDB, PostgreSQL, and MinIO PVCs. A Delete reclaim policy means the underlying storage volume is automatically removed when a PVC is deleted — a serious data-loss risk for production. Do not use node-local StorageClasses (such as local-path) for any stateful Plexicus component in production: the data lives on a single node and is irrecoverably lost if that node is replaced.

Check which StorageClasses are available in your cluster: kubectl get storageclass. Consult your cloud provider's documentation for the name of their Retain-policy block-storage class.

Create your ClusterIssuer after cert-manager is ready

helm install cert-manager installs only the controller and CRDs — it does not create a ClusterIssuer. After cert-manager pods reach Running, create one before you run helm install plexicus (the chart references it on every TLS-enabled Ingress). Example for Let's Encrypt HTTP-01:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <your-email>
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
ingressClassName: nginx # adjust to match your ingress controller

Set global.certManager.clusterIssuer in your values overlay to match metadata.name above. For additional issuer examples see the examples/ directory inside the chart artifact (helm pull --untar).

Plexicus depends on five infrastructure services: MongoDB, Redis, MinIO, PostgreSQL, and Temporal. Since chart 1.2.6, all five ship as opt-in bundled subcharts (enabled: false by default). This guide keeps separate Helm releases as the primary path — giving you independent version control, lifecycle management, and tuning per service. Customers who already operate any of these services can skip the corresponding install and point Plexicus at their existing endpoints via global.required.* and global.dependencies.* overrides.

Alternative: bundled infrastructure

Since chart 1.2.6, all five infra services can be enabled directly inside the umbrella chart. Add the following block to your values overlay instead of running separate Helm releases:

mongodb:
enabled: true
fullnameOverride: "mongodb"
auth:
rootPassword: "<same value as global.required.database.password>"

redis:
enabled: true
fullnameOverride: "redis"
auth:
password: "<same value as global.required.redis.password>"

minio:
enabled: true
fullnameOverride: "minio"
auth:
rootUser: minioadmin
rootPassword: "<same value as global.required.minio.rootPassword>"

temporal-postgresql:
enabled: true
fullnameOverride: "temporal-postgresql"
auth:
postgresPassword: "<same value as global.required.postgresql.password>"

temporal:
enabled: true
fullnameOverride: "temporal"

The fullnameOverride values produce the in-cluster Service names the chart expects (mongodb:27017, redis-master:6379, minio:9000, temporal-frontend:7233). The subchart auth passwords must equal the corresponding global.required.* values — mismatches cause runtime authentication failures that helm template does not detect.

For a complete single-command install using bundled infrastructure on k3s, see the Local Evaluation guide.

MongoDB replicaset mode

If you enable mongodb.architecture: replicaset for high-availability, do not set DATABASE_HOST=mongodb — that hostname resolves to the ClusterIP service, which is incompatible with the MongoDB replica-set protocol. Instead, use the full headless-service connection string that lists every replica member:

mongodb-0.mongodb-headless:27017,mongodb-1.mongodb-headless:27017,mongodb-2.mongodb-headless:27017/?replicaSet=rs0

In addition, create a mongodb-replica-set-key Kubernetes Secret containing a base64-encoded keyfile for intra-cluster replica-set authentication before the first MongoDB pod starts — the pod crashes on startup without it. Standalone architecture (mongodb.architecture: standalone) does not have these requirements and is simpler to operate for single-node deployments.

Bitnami licensing change (August 2025)

Bitnami moved the official chart images to a paid distribution. Free legacy images live under docker.io/bitnamilegacy/*. The commands below override every image reference (main + init containers + sidecars) to use the legacy registry. If you mirror these into your own registry, replace bitnamilegacy/ with your mirror path.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add temporal https://go.temporal.io/helm-charts
helm repo update

# 1. MongoDB
helm upgrade --install mongodb bitnami/mongodb --version 18.6.31 -n plexicus \
--set global.security.allowInsecureImages=true \
--set image.registry=docker.io \
--set image.repository=bitnamilegacy/mongodb \
--set volumePermissions.image.repository=bitnamilegacy/os-shell \
--set metrics.image.repository=bitnamilegacy/mongodb-exporter \
--set 'auth.rootPassword=<your-mongo-root-password>' \
--set 'auth.databases={plexicus}' \
--set 'auth.usernames={plexicus}' \
--set 'auth.passwords={<your-plexicus-db-password>}' \
--set persistence.size=10Gi --wait

# 2. Redis
helm upgrade --install redis bitnami/redis --version 25.5.1 -n plexicus \
--set global.security.allowInsecureImages=true \
--set image.registry=docker.io \
--set image.repository=bitnamilegacy/redis \
--set sentinel.image.repository=bitnamilegacy/redis-sentinel \
--set metrics.image.repository=bitnamilegacy/redis-exporter \
--set kubectl.image.repository=bitnamilegacy/kubectl \
--set volumePermissions.image.repository=bitnamilegacy/os-shell \
--set sysctl.image.repository=bitnamilegacy/os-shell \
--set auth.password=<your-redis-password> \
--set replica.replicaCount=0 \
--set master.persistence.size=4Gi --wait

# 3. MinIO
helm upgrade --install minio bitnami/minio --version 17.0.21 -n plexicus \
--set global.security.allowInsecureImages=true \
--set image.registry=docker.io \
--set image.repository=bitnamilegacy/minio \
--set defaultInitContainers.volumePermissions.image.repository=bitnamilegacy/os-shell \
--set console.image.repository=bitnamilegacy/minio-object-browser \
--set apiIngress.enabled=false --set ingress.enabled=false \
--set auth.rootUser=<your-minio-user> \
--set auth.rootPassword=<your-minio-password> \
--set defaultBuckets=platform \
--set persistence.size=10Gi --wait

# 4. PostgreSQL (for Temporal)
helm upgrade --install temporal-postgresql bitnami/postgresql --version 18.6.2 -n plexicus \
--set global.security.allowInsecureImages=true \
--set image.registry=docker.io \
--set image.repository=bitnamilegacy/postgresql \
--set metrics.image.repository=bitnamilegacy/postgres-exporter \
--set volumePermissions.image.repository=bitnamilegacy/os-shell \
--set auth.postgresPassword=<your-temporal-pg-password> \
--set primary.persistence.size=4Gi --wait

# 5. Temporal
# NOTE: --wait is intentionally omitted. Temporal pods crash-loop for ~2
# minutes while the schema-setup Job seeds the database — that is expected.
# Wait for the schema Job to complete (the section below polls the pods).
helm upgrade --install temporal temporal/temporal --version 0.73.2 -n plexicus \
--set image.registry=docker.io \
--set server.replicaCount=1 \
--set cassandra.enabled=false --set elasticsearch.enabled=false \
--set prometheus.enabled=false --set grafana.enabled=false \
--set server.config.persistence.default.driver=sql \
--set server.config.persistence.default.sql.driver=postgres12 \
--set server.config.persistence.default.sql.host=temporal-postgresql \
--set server.config.persistence.default.sql.port=5432 \
--set server.config.persistence.default.sql.database=temporal \
--set server.config.persistence.default.sql.user=postgres \
--set server.config.persistence.default.sql.password=<your-temporal-pg-password> \
--set server.config.persistence.visibility.driver=sql \
--set server.config.persistence.visibility.sql.driver=postgres12 \
--set server.config.persistence.visibility.sql.host=temporal-postgresql \
--set server.config.persistence.visibility.sql.port=5432 \
--set server.config.persistence.visibility.sql.database=temporal_visibility \
--set server.config.persistence.visibility.sql.user=postgres \
--set server.config.persistence.visibility.sql.password=<your-temporal-pg-password>

kubectl -n plexicus get pods # confirm all 5 prereqs are Running before continuing

The default in-cluster service names produced by these installs are mongodb, redis-master, minio, temporal-postgresql, and temporal-frontend. Chart 1.2.7+ defaults global.required.database.host, global.required.redis.host, global.required.minio.service, and the wait-loop endpoints to those names — so as long as you used the release names above, the customer overlay does not need to repeat them. Override only when connecting to externally-managed services or when ArgoCD prefixes the release names (see the ArgoCD section below).


7. Prepare Your Values File

Chart version

The remaining commands in this guide reference a $CHART_VERSION shell variable. Set it once to the version you want to install — your Plexicus contact will provide the current version when you receive registry credentials. To upgrade later, contact engineering@plexicus.ai for the latest version.

export CHART_VERSION=1.2.36

This variable persists for the rest of your shell session.

Instead of dumping the full 700-line default values file, start from the canonical customer overlay that ships inside the chart artifact. Pull and extract the chart, then copy the example overlay:

# Pull and extract the chart (creates a ./plexicus/ directory)
helm pull oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version $CHART_VERSION --untar

# Copy the canonical customer overlay
cp plexicus/values-customer.yaml.example my-values.yaml

# Edit my-values.yaml to replace every <to-fill> placeholder

The relevant contents of the overlay are shown below (~30 lines):

global:
# Your root domain — all service URLs are derived from this.
domain: plexicus.yourdomain.com # REQUIRED

scheme: "https"
wsScheme: "wss"

# Ingress controller class applied to the fastapi and frontend Ingress resources.
ingressClassName: "traefik" # also: "nginx", "gce", "alb"

# When enabled, the chart injects cert-manager.io/cluster-issuer on every
# Ingress that has TLS configured. Set enabled: false if you manage TLS yourself.
certManager:
enabled: true
clusterIssuer: "letsencrypt-prod"

imagePullSecrets:
- name: gar-secret

required:
ai:
# REQUIRED by values.schema.json — the render fails without them.
openAiDeploymentNameFree: "deepseek-ai/DeepSeek-V3"
openAiDeploymentSwe: "zai-org/GLM-5.2"

fastapi:
ingress:
enabled: true
hosts:
- host: api.plexicus.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: plexicus-fastapi-tls
hosts:
- api.plexicus.yourdomain.com
existingSecret: plexicus-fastapi

frontend:
ingress:
enabled: true
hosts:
- host: plexicus.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: plexicus-frontend-tls
hosts:
- plexicus.yourdomain.com
existingSecret: plexicus-frontend

worker:
existingSecret: plexicus-worker
envs:
# The worker resolves a non-BYOAI tenant's AI config from these at runtime.
# The chart templates them for fastapi only — without them here the worker
# falls back to api.openai.com and every AI call 401s. Change all three per
# channel together if you bring your own key.
AI_VALIDATION_PROVIDER: "deepinfra"
AI_VALIDATION_BASE_URL: "https://api.deepinfra.com/v1/openai"
AI_VALIDATION_MODEL: "deepseek-ai/DeepSeek-V4-Flash"
AI_REMEDIATION_PROVIDER: "deepinfra"
AI_REMEDIATION_BASE_URL: "https://api.deepinfra.com/v1/openai"
AI_REMEDIATION_MODEL: "deepseek-ai/DeepSeek-V4-Flash"
# No packaged-chart default for these three — the AI SAST scan engine and its
# mandatory embedding index have no endpoint to call without them. Chat can be
# any OpenAI-compatible provider; the embedding endpoint MUST serve BAAI/bge-m3
# (DeepSeek does not — use DeepInfra for it if your chat channel is DeepSeek).
PLEXICUS_AI_SCAN_BASE_URL: "https://api.deepinfra.com/v1/openai"
PLEXICUS_AI_SCAN_MODEL: "deepseek-ai/DeepSeek-V4-Flash"
PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL: "https://api.deepinfra.com/v1/openai"
analysis-scheduler:
existingSecret: plexicus-analysis-scheduler
codex-remedium:
existingSecret: plexicus-codex-remedium
exporter:
existingSecret: plexicus-exporter
plexalyzer-code:
existingSecret: plexicus-plexalyzer-code
plexalyzer-prov:
existingSecret: plexicus-plexalyzer-prov
The AI model ids are required — the install refuses to render without them

global.required.ai.openAiDeploymentNameFree and openAiDeploymentSwe are both required by values.schema.json, and the schema additionally rejects the old REPLACE_WITH_DEEPINFRA_MODEL_ID placeholder and the <to-fill> sentinel by pattern. Omitting them, or leaving a placeholder in place, fails at helm install / helm template time with an error naming the path.

This guard exists because the failure it replaces was invisible: a placeholder model id renders into AI_VALIDATION_MODEL, AI_REMEDIATION_MODEL and AI_CODEX_MODEL, and the platform only checks that the value is non-empty — so Settings → AI reported the tenant as configured while every call failed at the provider.

Set both to a real model id at your configured endpoint, for example:

global:
required:
ai:
openAiDeploymentNameFree: "deepseek-ai/DeepSeek-V3"
openAiDeploymentSwe: "zai-org/GLM-5.2"

The evaluation overlay (values-evaluation.yaml) sets its own pair, because Plexicus supplies the matching provider key with it.

global.domain carries more than ingress

global.domain is required and validated by values.schema.json — the install fails fast if it is empty, and the schema also rejects the placeholder plexicus.example.com so a copy-pasted overlay cannot reach a cluster.

It is not only the ingress hostname. The chart derives from it, among others:

  • the OAuth callback URLs sent to GitHub, GitLab and Bitbucket Cloud (<scheme>://<domain>/api/callback/<provider>), and
  • the WebAuthn relying party used by passkeys — WEBAUTHN_RP_ID is the bare host and WEBAUTHN_ORIGIN is <scheme>://<host>. Before these were derived, self-hosted installs fell back to localhost and the browser refused every passkey registration without surfacing an error. If you serve the interface from several hostnames under one parent and want a shared passkey, override WEBAUTHN_RP_ID with the parent domain — that choice cannot be derived safely.

Changing global.domain after users have registered passkeys invalidates them: a credential is bound to the RP ID it was created under.

Why this overlay is short

Chart 1.2.7+ defaults the in-cluster service names (mongodb, redis-master, minio:9000), the cross-service URLs (FastAPI's importer / notification / receive endpoints), the AI endpoint (DeepInfra's OpenAI-compatible API), and the Cloudflare Turnstile keys (test tokens for eval). All sensitive values (passwords, OAuth client secrets, API keys, real Turnstile keys for production) come from the eight existingSecret references — they are never written into my-values.yaml.

Override entries in global.required.* only when:

  • you connect the chart to externally-managed MongoDB / Redis / MinIO / Temporal (different host/port than the prereq install in step 6 produced),
  • you use a non-default release name for the prereqs (e.g. ArgoCD prefixes them — see the ArgoCD section below),
  • you run production and need to set real Cloudflare Turnstile keys, real OAuth client_id values, or your own AI provider URL.

Cloudflare Turnstile (production)

Plexicus' login form is gated by Cloudflare Turnstile. The chart defaults to Cloudflare's documented "always passes" test tokens (1x00000000000000000000AA / 1x0000000000000000000000000000000AA) so that fresh installs reach the login form without further configuration. For any production deployment you must replace both with real keys generated at dash.cloudflare.com → Turnstile — leaving the test tokens in place effectively disables bot protection.

global:
required:
turnstile:
siteKey: "0x4AAAAAAA…YOUR-SITEKEY" # public — rendered into the login page
secretKey: "0x4AAAAAAA…YOUR-SERVER-SECRET" # private — server-side validation
successToken: "XXXX.DUMMY.TOKEN.XXXX" # used only for end-to-end test fixtures

Both siteKey and secretKey are required for the Nuxt frontend to render the Turnstile widget AND to verify tokens server-side at /api/_turnstile/validate. Mismatched or empty values surface as either skeleton-loader inputs that never resolve (sitekey wrong) or Turnstile validation failed, Please try again after submitting the form (secret wrong).

CORS allow-list

The chart's fastapi.envs.CORS_ORIGINS defaults to <scheme>://<domain>,<scheme>://api.<domain> so the SPA at the same domain plus the api subdomain works out of the box. Override fastapi.envs.CORS_ORIGINS in your customer overlay only if you front the SPA on additional hostnames (multi-tenant, branded subdomains, embedded iframe). An empty CORS_ORIGINS makes every cross-origin request return 400 Disallowed CORS origin.

Ingress class and TLS issuer are global

global.ingressClassName (default: traefik) and global.certManager.enabled + global.certManager.clusterIssuer (default: letsencrypt-prod) apply to the fastapi and frontend Ingress resources automatically. Per-service overrides via <service>.ingress.className or <service>.ingress.annotations are possible but rarely needed.

Only fastapi and frontend are externally reachable. The other seven services (worker, analysis-scheduler, codex-remedium, exporter, plexalyzer-code, plexalyzer-prov, and AI Pentest) are cluster-internal and do not need Ingress.

Production hardening — OAUTHLIB_INSECURE_TRANSPORT

The bundled-infra overlay (values-bundled-infra.yaml) sets OAUTHLIB_INSECURE_TRANSPORT: "0", which is correct. Never set this value to "1" in any deployment that uses TLS — it disables the OAuth library's HTTPS enforcement and allows OAuth token exchanges over plain HTTP. If you copied or adapted the bundled-infra overlay for production, verify this variable is either absent or explicitly set to "0" in your fastapi.envs block.


8. Install the Chart

helm upgrade --install plexicus \
oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version $CHART_VERSION \
--namespace plexicus \
--values my-values.yaml

The installation takes a few minutes while all pods and dependencies initialize. upgrade --install is idempotent — re-running it after fixing a values typo does the right thing instead of erroring with release plexicus already exists.


9. Verify the Installation

# All pods should reach Running or Completed status
kubectl get pods -n plexicus

# Ingress should have an external address assigned
kubectl get ingress -n plexicus

Pods typically reach a healthy state within 3–5 minutes. If a pod is stuck in CrashLoopBackOff or Error, inspect its logs:

kubectl logs -n plexicus <pod-name> --previous

Common causes are missing secret keys or incorrect connection details in my-values.yaml.

nota

After install, Helm prints a NEXT STEPS banner (templates/NOTES.txt) with verification commands tailored to your release. Re-display it any time with:

helm get notes plexicus -n plexicus

A first end-to-end smoke test from outside the cluster:

curl -s -o /dev/null -w "Frontend  %{http_code}\n" https://<your-domain>/
curl -s -o /dev/null -w "API/health %{http_code}\n" https://api.<your-domain>/health
# Frontend 302 (the SPA's login redirect — Tier A passes)
# API/health 200 (FastAPI is running — liveness only; does not probe MongoDB / Redis / MinIO / Temporal)

A trusted-TLS verification (no -k needed when using a public ClusterIssuer):

curl -s -o /dev/null -w "TLS verify=%{ssl_verify_result}\n" https://<your-domain>/
# TLS verify=0 (publicly trusted certificate)

10. Bootstrap the First Admin User

The chart ships with an empty database. Plexicus does not seed an initial admin during install — every account is created through the registration API. The default registration flow expects email verification via SMTP; on a brand-new install the very first admin therefore has to be either:

  • (Recommended for production) registered through the regular UI flow once SMTP credentials (global.required.smtp.* values plus the EMAIL_PASSWORD Secret key — all surfaced in-container as EMAIL_*) are wired into plexicus-fastapi (the user clicks the verification link in the welcome email), or
  • (Bootstrap-only shortcut) registered via the API and then flagged is_verified: true directly on the Users MongoDB collection — the only option on a deployment with no mail server, for the reason explained below.

10.0 Decide your email delivery mode first

global.required.smtp.deliveryMode (rendered as EMAIL_DELIVERY_MODE) takes "smtp" (the default) or "disabled", and it changes what the registration API does. Set it deliberately before you bootstrap:

deliveryModesmtp.serverWhat happens
"smtp" (default)setNormal behaviour. Verification, invitation and password-reset mail is sent.
"smtp" (default)emptyEvery email-dependent call fails with 503 — register, invite, resend-verification and password reset. This is the "you forgot to configure SMTP" state and it is deliberately loud.
"disabled"ignoredThe deployment declares it runs without email. Nothing is sent, responses carry email_sent: false, and onboarding becomes admin-mediated.

The 503 is an RFC 7807 application/problem+json body whose detail names both escapes — set EMAIL_HOST, or declare EMAIL_DELIVERY_MODE=disabled. Before this existed, an install with no SMTP returned 200 and told the user to check an inbox nothing was ever sent to; that is what the 503 replaces.

disabled does not auto-verify self-registrations — and the first admin still needs the MongoDB shortcut

It is tempting to read deliveryMode: "disabled" as "skip verification". It is not. POST /registrations writes is_verified: false in every delivery mode, by design: it is a public, unauthenticated endpoint with no backend captcha, and each registration mints a fresh tenant carrying a trial with AI credits — so auto-verifying there would turn "we have no mail server" into open, billable signup for anyone who can reach the host.

What disabled gives you instead is an admin-mediated path: an already-authenticated admin invites a user (POST /teams/self/members, or the superadmin POST /backoffice/users/invite) and the invite response itself carries a verification_link field to hand over out of band. That link is returned only to an authenticated admin, and only when the mode was explicitly declared — merely forgetting to configure SMTP never discloses one.

That path cannot bootstrap the first admin, because it presupposes an admin. On a fresh cluster with no mail server, step 10.2 below remains the only way in.

To declare a deployment as running without email, set it in your overlay:

global:
required:
smtp:
deliveryMode: "disabled"

The evaluation overlay (values-evaluation.yaml) already ships disabled.

Anything that is not disabled means smtp

The value is not a validated enum — values.schema.json does not constrain it. A typo such as "disbaled" is silently treated as "smtp", which puts you in the 503 row above. Check the rendered value if register starts returning 503 unexpectedly.

10.1 Register the user via the API

The schema validator requires at least one uppercase letter, one lowercase letter, one digit, and one special character:

curl -s -X POST -H "Content-Type: application/json" \
-d '{
"email":"admin@your-domain.example",
"password":"ChangeMeNow1!",
"confirm_password":"ChangeMeNow1!"
}' \
https://api.<your-domain>/registrations
# {"success":true,"message":"User successfully registered. Please check your email for verification link."}

With deliveryMode: "disabled" the same call returns 200 with an extra email_sent: false and a different message — "…This deployment runs without email delivery, so no verification email was sent — ask an administrator to activate your account." The account exists but is not verified either way.

A 503 here means SMTP is neither configured nor declared absent — go back to step 10.0.

If SMTP is wired up, the user clicks the link in the welcome email and is done. Skip to step 10.3.

10.2 (Bootstrap shortcut) Manually verify the user in MongoDB

Use only for the first admin on a deployment with no mail server

This shortcut writes directly to MongoDB. In production, prefer wiring real SMTP credentials and using the verification email. Use it only for the very first admin in eval / staging / air-gapped clusters — after that, invite users from inside the product, which under deliveryMode: "disabled" hands the inviting admin a verification_link to pass on. Setting deliveryMode: "disabled" does not remove the need for this step; see 10.0.

The collection name is Users (capital U); the verification flag is is_verified:

kubectl -n plexicus exec deploy/mongodb -- mongosh \
# ↑ if you installed via the ArgoCD path the deploy is named `plexicus-mongodb` —
# substitute `deploy/plexicus-mongodb` in this command and the next.
#
"mongodb://root:<your-mongo-root-password>@localhost:27017/plexicus?authSource=admin" \
--quiet --eval '
db.Users.updateOne(
{ email: "admin@your-domain.example" },
{ $set: { is_verified: true, role: "admin" } }
)
'
# { acknowledged: true, matchedCount: 1, modifiedCount: 1 }

10.3 Log in

curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":"admin@your-domain.example","password":"ChangeMeNow1!"}' \
https://api.<your-domain>/sessions
# {"access_token":"eyJhbGc…","token_type":"bearer"}

A 200 with an access_token confirms the full chain — frontend → ingress → fastapi → MongoDB authentication — is healthy. Open https://<your-domain> in a browser and sign in with the credentials you just registered.


11. Connect a SCM and run your first scan

The first run-through has two paths: a Sandbox option that uses Plexicus' pre-configured vulnerable repository (no OAuth required, useful for verifying the install end-to-end before integrating with your real Git host) and a Production / Connect SCM option that wires the platform to GitHub / GitLab / Bitbucket / Gitea via OAuth.

The screenshots below were captured against a working install at https://demo.plexicus.com. Substitute your own hostname.

11.1 Sign in to the panel

Open https://<your-domain> in your browser and sign in with the admin user you bootstrapped in step 10.

Plexicus login page

11.2 Accept the Beta Agreement

The first time any user logs in, a modal asks them to read and accept the Plexicus Beta Agreement. Scroll the modal to the bottom — the Continue button is disabled until the inner scrollbar reaches the bottom — then click Continue.

Plexicus Beta Agreement modal — must scroll to bottom

11.3 Pick your onboarding path

Onboarding offers two cards: Connect SCM (production — real OAuth redirect) or Go To Sandbox (eval — pre-configured vulnerable repo, no OAuth).

Onboarding screen offering Connect SCM or Go To Sandbox

Sandbox path (recommended for the first scan)

Click Go To Sandbox. Plexicus presents a catalogue of pre-configured repositories — simplest-vulnerable is the canonical demo target. Click the card to select it.

Sandbox repository selection screen

The branch dropdown defaults to main. Confirm and click Continue.

Sandbox repo selected, Continue enabled

The wizard advances to the scanning step and stays there while the scan runs. A spinning progress bar and status label track overall completion. A live scan terminal appears as a collapsible drawer pinned to the bottom of the page — it streams per-tool events from the worker as they arrive. The sandbox scan of simplest-vulnerable typically completes in 2 minutes, at which point the wizard advances automatically to the Dashboard.

Production path — register OAuth apps (any time after install)

Already installed? Nothing needs reinstalling

OAuth apps are registered against a running deployment. "First" below means before clicking Connect in the UI — not before installing the chart. A deployment installed without any SCM credentials adds them later via Settings → Integrations (GitHub App, automatic) or the values + Secret path in Wire the credentials into Plexicus.

For production deployments, Connect SCM uses a real OAuth 2.0 redirect flow. Each provider must have an OAuth application registered on its side, with a callback URL that exactly matches the URL Plexicus will redirect to. If the registered callback doesn't match, the provider rejects the request with 400 redirect_uri_mismatch (GitHub) or equivalent.

Substitute <your-domain> (e.g. plexicus.acme.com) in every URL below. Sign-in and the repository connector both land on the SPA domain, with no api. prefix — with one exception, called out in the table.

ProviderAuthorization callback URL to register
GitHubhttps://<your-domain>/api/callback/github — sign-in and the repository connector
GitHub (second URL, Apps only)https://api.<your-domain>/vulnerability-tool/callback/github — the vulnerability-tool flow and the App-installation hand-off. Served by the API, so this one does carry the api. prefix; note the hyphen in vulnerability-tool
GitLabhttps://<your-domain>/api/callback/gitlab
Bitbucket Cloudhttps://<your-domain>/api/callback/bitbucket_cloud
Google (SSO)https://<your-domain>/api/callback/google
A GitHub App needs both of its callback URLs registered

Use Add Callback URL on the app settings page to register the second one. Registering only the SPA-domain URL leaves the App-installation hand-off dead-ending; registering only the API-domain one breaks sign-in and the connector with redirect_uri_mismatch. GitLab and Bitbucket Cloud have exactly one callback each — the SPA-domain one.

The eval install at https://plexicus.local needs no OAuth apps to stand up and run its first scan: the Sandbox path scans a pre-configured repository through a synthetic SCM connector. Register an OAuth app only when you want to scan your own repositories — see Connect your own SCM. The callback URLs are then the ones above with your PLEXICUS_DOMAIN in place of <your-domain>; a .local name works, because the browser is what follows the redirect. Inbound webhooks are the exception — see SCM Connection Reference → Webhooks.

Full per-provider reference

This section covers the three providers whose credentials live in the chart. For every supported provider — including GitHub Enterprise Server, self-managed GitLab, Gitea, Forgejo and Azure DevOps — with its flow, callbacks, scopes and env vars in one table, see the SCM Connection Reference.

GitHub — register an OAuth App

For per-user OAuth (the simplest path):

  1. On GitHub, navigate to Settings → Developer settings → OAuth Apps → New OAuth App (direct link). For organisation-wide installs, register the app under the organisation's settings instead of your personal account.
  2. Fill the form:
    • Application name: Plexicus (or any label your users will see on the consent screen)
    • Homepage URL: https://<your-domain>
    • Authorization callback URL: https://<your-domain>/api/callback/github ← exactly this path
    • Application description: optional
    • Leave Enable Device Flow unchecked
  3. Click Register application.
  4. On the resulting page, copy the Client ID (visible) and click Generate a new client secret to reveal the Client Secret (shown only once — copy it before navigating away).

An OAuth App supports a single callback URL, which is why it can serve sign-in and the connector but not the App-installation hand-off.

For richer integration (webhooks, branch checks, repo-level metadata), use a GitHub App instead: Settings → Developer settings → GitHub Apps → New GitHub App. Register both callback URLs from the table above (Add Callback URL adds the second), check Request user authorization (OAuth) during installation, and grant the repository permissions Metadata: Read-only, Contents: Read-only, Pull requests: Read & Write (remediation pull requests), Webhooks: Read & Write (push-triggered rescans). Download the generated private-key .pem file and note the App ID and app slug — the .pem goes into GITHUB_APP_PRIVATE_KEY (base64-encoded) and the App ID into GITHUB_APP_ID, alongside the OAuth client credentials. Then install the app on your account or organisation; https://github.com/apps/<slug>/installations/new is the value for appInstallationUrl.

Plexicus sends no scope parameter on the GitHub authorize URL — a GitHub App's access comes entirely from the permissions above plus the repositories it is installed on. That is also why a plain OAuth App grants less than you may expect.

GitLab — register an OAuth Application
  1. On GitLab, navigate to User settings → Applications → New application (direct link) — or Admin Area → Applications for instance-wide registration on self-hosted GitLab.
  2. Fill the form:
    • Name: Plexicus
    • Redirect URI: https://<your-domain>/api/callback/gitlab
    • Confidential: keep checked
    • Scopes — Plexicus requests all eight, so grant all eight: api, read_api, read_user, read_repository, write_repository, openid, profile, email. Granting fewer lets the authorization succeed and the later repository calls fail.
  3. Click Save application.
  4. Copy the Application ID (= clientId) and the Secret shown on the next page (visible only once).
Bitbucket Cloud — register an OAuth Consumer
  1. On Bitbucket, navigate to Workspace settings → OAuth consumers → Add consumer (go to Bitbucket Cloud and pick the workspace).
  2. Fill the form:
    • Name: Plexicus
    • Callback URL: https://<your-domain>/api/callback/bitbucket_cloud
    • URL: https://<your-domain> (homepage)
    • This is a private consumer: keep checked
    • Permissions: Account Read; Repositories Read; Pull requests Read (add Write for inline-comment integration)
  3. Click Save.
  4. Expand the new consumer entry to reveal the Key (= clientId) and Secret (= clientSecret).
Wire the credentials into Plexicus

OAuth credentials split into two halves: the client_id is public (rendered into the SPA's Login button URL) and the client_secret is private (used only by the FastAPI backend and the Nuxt SSR layer to exchange the OAuth code for an access token).

Public IDs go into your customer values overlay (my-values.yaml):

global:
required:
oauth:
github:
clientId: "Iv1.abcdef0123456789" # GitHub OAuth App "Client ID"
# Leave freeSastToolUrl unset. Despite the name it is what fastapi
# receives as GITHUB_OAUTH_REDIRECT_URI, and when empty the chart
# derives https://<your-domain>/api/callback/github — the callback the
# SPA actually sends. Set it only for the Plexicus free-scan funnel.
# appId / appPrivateKey only if you registered a GitHub App (not OAuth App).
# QUOTE appId — see the warning below:
# appId: "1234567"
# appInstallationUrl: "https://github.com/apps/<your-app-slug>/installations/new"
gitlab:
clientId: "abcdef0123456789abcdef..." # GitLab Application ID
bitbucket:
key: "abcdef0123456789abcd" # Bitbucket Consumer Key
google:
clientId: "1234567890-abcdef.apps.googleusercontent.com"
Every OAuth provider now defaults to empty

github, gitlab, bitbucket and google all default to "", so omitting a provider disables it cleanly — the connector is hidden rather than shown behind a button that cannot work.

The chart's values.schema.json also rejects two literals by value: Plexicus's own GitHub App id and Plexicus's own GitHub OAuth client id. Both once shipped as defaults, which silently pointed self-hosted installs at an App that Plexicus owns. An overlay that reintroduces either fails at render time.

Do not write appId: 0: the schema types it as a string, so a bare 0 fails validation, and "" is the value that means "no GitHub App".

Quote appId in every overlay

GITHUB_APP_ID must be a quoted string in YAML. Helm round-trips values through JSON, where numbers become Go float64, and Go's shortest-form printer switches to scientific notation at 1,000,000 — so an unquoted 7-digit appId: 1234567 reaches the container as 1.234567e+06, which no GitHub API accepts. Nothing fails at render time; App-authenticated calls simply stop working. Write appId: "1234567". The production and staging overlays quote theirs, which is why this only ever bit installs that took the default.

You can skip all of this — let the product create the App

Since the setup wizard was retired, GitHub App creation lives in the product itself. On a deployment with no App configured, Settings → Integrations → GitHub offers Create GitHub App automatically, which walks GitHub's App Manifest flow and stores the resulting credentials. A stored App takes precedence over these chart values in both the backend and the Nuxt login proxy, so you can leave appId, appInstallationUrl and appPrivateKey unset entirely. See SCM Connections → Let Plexicus create the App for you for what the generated App covers and what it does not.

Private secrets go into the plexicus-fastapi Kubernetes Secret you created in step 5 (re-create with the new keys, or kubectl edit secret to add them):

kubectl -n plexicus create secret generic plexicus-fastapi \
... existing keys ... \
--from-literal=GITHUB_OAUTH_CLIENT_SECRET="<github-oauth-client-secret>" \
--from-literal=GITLAB_OAUTH_CLIENT_SECRET="<gitlab-application-secret>" \
--from-literal=BITBUCKET_OAUTH_CLIENT_SECRET="<bitbucket-consumer-secret>" \
--dry-run=client -o yaml | kubectl apply -f -

If you registered a GitHub App (not an OAuth App), also add the private key:

kubectl -n plexicus patch secret plexicus-fastapi \
--type=json -p='[{"op":"add","path":"/data/GITHUB_APP_PRIVATE_KEY","value":"'$(base64 -w0 < ~/Downloads/your-app.private-key.pem)'"}]'
kubectl -n plexicus patch secret plexicus-worker \
--type=json -p='[{"op":"add","path":"/data/GITHUB_APP_PRIVATE_KEY","value":"'$(base64 -w0 < ~/Downloads/your-app.private-key.pem)'"}]'

Roll the workloads so the new env vars take effect:

helm upgrade plexicus oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version $CHART_VERSION --namespace plexicus -f my-values.yaml
kubectl -n plexicus rollout restart deploy/fastapi deploy/frontend deploy/worker
Now click Connect SCM

After the rollout completes, return to the Connectors page, find your provider, and click Connect:

  1. Open the Connectors entry in the left sidebar.
  2. Find your SCM (GitHub / GitLab / Bitbucket) in the SCM Integrations panel and click Connect.
  3. Plexicus redirects to the provider's authorize URL. Log in and click Authorize Plexicus.
  4. The provider redirects back to https://<your-domain>/api/callback/<provider> and the connector card displays Connected.

If you see redirect_uri_mismatch (GitHub) or invalid_redirect_uri (GitLab/Bitbucket) in the browser address bar after step 3, the Authorization callback URL registered with the provider does not match what Plexicus sent. Re-check it: the value must be exactly https://<your-domain>/api/callback/<provider> — same scheme (https vs http), same hostname (no api. prefix for this callback), no trailing slash, no path query string. The api.-prefixed /vulnerability-tool/callback/github URL is a second, additional registration on GitHub Apps — it never replaces this one.

The OAuth redirect is browser-only — there is no Personal Access Token or API shortcut. Once a SCM is connected through the browser, every subsequent operation is available over the REST API for CI / scripting (POST /repositories/bulk, POST /repository-scans, GET /findings).

Add a repository and scan

After Connect succeeds, the Applications page replaces the Sandbox flow:

  1. Open Applications in the left sidebar and click Add applications.
  2. Pick the SCM connector you just authorized.
  3. Choose a repository, set a nickname, pick the branch (defaults to main).
  4. Click Create selected repositories.
  5. Open the new repository entry and click Run scan.

11.4 Land on the Dashboard

After the scan completes Plexicus drops you on the Dashboard, which summarises platform value (saved cost, returned engineering time, comparison vs industry baseline) and surfaces the Findings Report panel ready to populate as more scans accumulate.

Plexicus Dashboard after first scan

11.5 Review the repository in Assets

Open Assets in the left sidebar, or open the Activity Center tray and click a completed scan to jump straight to its repository. Scan status lives in the Activity Center's six-phase path (Queued → Preparing environment → Scanning → Processing results → Completed → Enriched) rather than a badge on the Assets row — expand the scan's side panel for live per-tool progress, or check the History filter pill for past runs. The FINDINGS, PIPELINE, PRIORITY, and TAGS columns on the Assets row populate as enrichment completes.

Activity Center side panel open on a scan in the Preparing environment phase

Assets list with simplest-vulnerable repository post-scan

11.6 Open the Findings page

Open Findings in the left sidebar. The page splits into Repo / SCM / Cloud / Registry tabs (Repo is the default). On a brand-new install, the default view often shows the "Zero findings remaining" empty state — the SAST scanner produced findings, the AI enriched them, and they sit in the enriched / completed state until you act on them.

Findings page empty state on a new install

Click the funnel icon next to the search box and include statuses like enriched and completed to make every finding visible, or open the repository tile from Assets (11.5) and drill in for a per-repo view. From there each finding shows the affected file, the matched rule, and the AI-generated remediation patch.


Day 2 References

The chart artifact bundles operator documentation under docs/. After running helm pull --untar (see step 7), the following guides are available locally:

ls plexicus/docs/
  • getting-started.md — customer install walk-through
  • secrets-management.md — secrets catalog and ESO/Sealed Secrets recipes
  • ingress-tls.md — ingress and TLS configuration
  • image-registry.md — image mirroring and air-gapped deployments
  • upgrading.md — how to upgrade to a new chart version and roll back
  • troubleshooting.md — common install failures and fixes
  • uninstall.md — clean removal of all chart resources

These ship with the chart and stay in sync with the version you installed. For environment-specific guidance, contact engineering@plexicus.ai.

For backup and restore procedures (MongoDB, PostgreSQL, object storage), see the Backup and Restore guide.


12. Upgrading

Pre-upgrade checklist

Check your overlay for a pinned redis.websocketDb

global.required.redis.websocketDb now defaults to 2, aligning the chart with the default every reader in the platform already used. It was previously 1, which matched no deployed environment and no code default.

If your overlay pins it — most do not — either remove the pin or set it to 2. Redis pub/sub is scoped per database, so a publisher and a subscriber pointed at different indices simply stop seeing each other: live scan progress goes quiet with no error anywhere. The other indices in use are 0, 2 and 3; nothing claims 1.

Back up data before every upgrade

Database schema migrations may run automatically during helm upgrade. If the upgrade fails mid-migration, the database may be in a state that the old chart version cannot read. A backup taken immediately before the upgrade is your only guaranteed rollback path for the data layer.

See the Backup and Restore guide for the full procedure.

# 1. Review release notes from the Plexicus team before running the upgrade.
# Breaking changes or required values migrations are always listed there.

# 2. Take a backup immediately before upgrading.
# (Full commands in the Backup and Restore guide.)

# 3. Pin the new version and upgrade:
helm upgrade plexicus \
oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version <new-version> \
--namespace plexicus \
--values my-values.yaml

Post-upgrade verification

After the upgrade, confirm the platform is healthy before considering the upgrade complete:

# All pods must be Running or Completed — no Pending or CrashLoopBackOff
kubectl get pods -n plexicus

# API health endpoint must return 200
curl -s -o /dev/null -w "%{http_code}" https://api.<your-domain>/health
# Expected: 200

# Smoke test: open https://<your-domain> in a browser, log in, and run a scan

The upgrade is successful when all pods reach Running, the /health endpoint returns 200, and the login flow completes without errors.

Rollback

If the post-upgrade verification fails, roll back to the previous Helm release revision:

# List available revisions
helm history plexicus -n plexicus

# Roll back to the previous revision (or specify a revision number)
helm rollback plexicus -n plexicus
Rollback does not downgrade database schemas

helm rollback restores the previous Helm release state — it does not revert database schema migrations that may have run during the upgrade. If the schema was migrated forward, the rolled-back application pods may fail to connect to the database. In that case, restore the database from the backup you took before the upgrade rather than relying on helm rollback alone.


ArgoCD Deployment (Alternative)

If you manage your cluster with ArgoCD, you can drive the Plexicus chart directly from OCI instead of the Helm CLI. The recommended pattern is a single Application per environment that uses the umbrella chart with bundled infrastructure subcharts enabled — no separate MongoDB / Redis / PostgreSQL / Temporal Applications needed.

Validated end-to-end

The procedure below was validated on k3s + ArgoCD v3, Helm chart 1.2.19; it applies unchanged through the current 1.2.36 release. The reference manifests live inside the chart artifact at argocd/application.yaml — extract with helm pull --untar and adapt to your environment. Key gotchas:

  • The AppProject's sourceRepos list must include every chart source you reference. When using bundled subcharts, you only need the OCI GAR registry and your GitOps Git repo — the Bitnami and Temporal repositories are not required.
  • ArgoCD ≤ 2.12 does not support semver ranges for OCI sources. Always pin targetRevision to an exact version string (e.g. "1.2.36").
  • Image-tag drift. The chart pins <service>.image.tag to the chart version, but GAR images use independent build numbers. Override <service>.image.tag: latest for evaluation or pin specific build tags for reproducible production deployments.
1

Create the GitOps repository and AppProject

ArgoCD pulls Application manifests from a Git repository. Even when the chart lives in OCI, the Application manifest and any environment-specific values live in Git so every change is auditable, peer-reviewable, and revertable.

Recommended layout for a single-environment deployment:

plexicus-gitops/
├── README.md
├── projects/
│ └── plexicus-appproject.yaml # Restricts allowed sources/destinations
├── apps/
│ ├── root-app.yaml # App-of-Apps root (optional, see tip below)
│ ├── plexicus-app.yaml # Umbrella Application
│ └── secrets-app.yaml # Sibling Application for SealedSecrets
├── environments/
│ └── prod/
│ └── values.yaml # Non-sensitive overlay (committed to Git)
└── secrets/
└── README.md # Pointer to sealed secret YAML files

Initialize and push the repo from your operator workstation:

cd ~/plexicus-deploy
mkdir -p plexicus-gitops/{projects,apps,environments/prod,secrets}
cd plexicus-gitops
git init -b main
cat > .gitignore <<'EOF'
repo-gar.yaml
sa-key.json
*.key
EOF
git add .gitignore && git commit -m "chore: bootstrap plexicus GitOps repo"
git remote add origin git@github.com:<your-org>/plexicus-gitops.git
git push -u origin main

Register the Git repo with ArgoCD. Apply a Secret in the argocd namespace — do NOT commit this file (it contains credentials):

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: plexicus-gitops-repo
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
type: Opaque
stringData:
type: git
url: git@github.com:<your-org>/plexicus-gitops.git
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
<paste deploy key>
-----END OPENSSH PRIVATE KEY-----
EOF

AppProject — restrict allowed sources. When using bundled infrastructure subcharts, you only need two source entries — the OCI registry and your Git repo. The Bitnami and Temporal chart repositories are not needed.

Save as projects/plexicus-appproject.yaml:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: plexicus
namespace: argocd
spec:
description: Plexicus platform deployment
sourceRepos:
- https://github.com/<your-org>/plexicus-gitops.git
- europe-west3-docker.pkg.dev/plexicus-registry/charts
destinations:
- namespace: plexicus
server: https://kubernetes.default.svc
- namespace: argocd
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: cert-manager.io
kind: ClusterIssuer
namespaceResourceWhitelist:
- group: "*"
kind: "*"

Secrets handling — never commit raw Secret YAML. Pick one approach:

  • Sealed Secrets — encrypt with a cluster-specific public key, commit the SealedSecret ciphertext, controller decrypts in-cluster. Simplest for a single cluster. See step 3 for the critical key-backup procedure.
  • External Secrets Operator (ESO) — ExternalSecret CRD pulls from Vault, AWS Secrets Manager, GCP Secret Manager, etc.
  • SOPS with helm-secrets or argocd-vault-plugin.

Every Application manifest lives under apps/, committed and pushed before applying.

2

Configure the OCI repository secret

The Plexicus chart lives in OCI, so ArgoCD needs GAR credentials. Create repo-gar.yaml — do NOT commit it:

apiVersion: v1
kind: Secret
metadata:
name: plexicus-gar
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
type: Opaque
stringData:
type: helm
name: plexicus-gar
url: europe-west3-docker.pkg.dev/plexicus-registry/charts
enableOCI: "true"
username: _json_key
password: |
<CONTENTS OF sa-key.json>
kubectl apply -f repo-gar.yaml
3

Deploy application secrets

The nine plexicus-* Secrets from step 5 of the Helm-CLI path must exist before the Application syncs. Bootstrap them out-of-band with kubectl create secret (as in step 5), or manage them declaratively via Sealed Secrets.

If using Sealed Secrets:

Install the controller:

helm upgrade --install sealed-secrets \
oci://registry-1.docker.io/bitnamicharts/sealed-secrets \
--namespace kube-system --create-namespace
Back up your sealing key immediately

The sealing key is cluster-unique and generated once at controller startup. Every SealedSecret you commit can only be decrypted by the cluster that holds this key. If the cluster is lost and you have no key backup, all sealed secrets are permanently unreadable — you must re-seal every credential against the new cluster's key.

Back up the key immediately after installing the controller and store it offline:

kubectl -n kube-system get secret \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > sealed-secrets-key.backup.yaml
# Store this file offline — never commit it to Git.

To restore a key to a replacement cluster, apply the backup YAML before the controller starts for the first time, so it picks up the existing key rather than generating a new one.

Version note: pin the sealed-secrets controller chart version before upgrading it. The sealing key is backward compatible across controller versions (a newer controller can decrypt secrets sealed by an older version), but verify the project's release notes for the version span you are crossing.

Seal each Secret:

kubectl create secret generic plexicus-fastapi \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
... \
--dry-run=client -o yaml | \
kubeseal --controller-namespace=kube-system --format=yaml \
> secrets/sealed-plexicus-fastapi.yaml
# Repeat for every plexicus-* secret including plexicus-license

Commit and push the sealed files; ArgoCD reconciles them before the platform syncs (use a sibling secrets-app.yaml Application with sync-wave -1).

4

Create the umbrella Application

The recommended pattern is a single Application pointing at the OCI chart, with all infrastructure subcharts bundled inside and values supplied inline (or from a valueFiles reference to your Git repo).

Save as apps/plexicus-app.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: plexicus
namespace: argocd
spec:
project: plexicus
source:
repoURL: europe-west3-docker.pkg.dev/plexicus-registry/charts
chart: plexicus
targetRevision: "1.2.36"
helm:
values: |
global:
domain: plexicus.yourdomain.com
scheme: https
wsScheme: wss
ingressClassName: "nginx"
certManager:
enabled: true
clusterIssuer: "letsencrypt-prod"
imagePullSecrets:
- name: gar-secret
required:
# Object storage — point at your S3-compatible service
# (hostname only, no https://, no trailing slash)
minio:
service: "<your-s3-host>"
buckets: "<your-bucket-name>"
rootUser: "<access-key-id>"
rootPassword: "<secret-access-key>"

# Bundled infrastructure subcharts — all enabled inside the umbrella chart.
# fullnameOverride pins the in-cluster Service names to the chart's defaults
# so no global.required.database.host / redis.host overrides are needed.
mongodb:
enabled: true
fullnameOverride: "mongodb"
auth:
rootPassword: "<mongo-root-password>" # use SealedSecret or ESO

redis:
enabled: true
fullnameOverride: "redis"
auth:
password: "<redis-password>" # use SealedSecret or ESO

minio:
enabled: false # disabled — using external S3 above

temporal-postgresql:
enabled: true
fullnameOverride: "temporal-postgresql"
auth:
postgresPassword: "<pg-password>" # use SealedSecret or ESO

temporal:
enabled: true
fullnameOverride: "temporal"

fastapi:
existingSecret: plexicus-fastapi
worker:
existingSecret: plexicus-worker
frontend:
existingSecret: plexicus-frontend
analysis-scheduler:
existingSecret: plexicus-analysis-scheduler
codex-remedium:
existingSecret: plexicus-codex-remedium
exporter:
existingSecret: plexicus-exporter
plexalyzer-code:
existingSecret: plexicus-plexalyzer-code
plexalyzer-prov:
existingSecret: plexicus-plexalyzer-prov
destination:
server: https://kubernetes.default.svc
namespace: plexicus
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
App-of-Apps

For a fully self-contained GitOps loop, define a single "root" Application pointing at apps/ in your Git repo (App-of-Apps pattern). Then the only kubectl apply you ever run manually is the root manifest — every subsequent change goes through git push.

Alternative: separate prereq Applications

The chart also ships reference manifests for a five-separate-Applications pattern (one each for MongoDB, Redis, MinIO, PostgreSQL, Temporal at sync-wave 0, umbrella at sync-wave 5). This gives independent lifecycle control over each infra service and mirrors the Helm-CLI separate-release path from step 6. Extract the manifests from the chart artifact:

helm pull oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version $CHART_VERSION --untar
ls plexicus/argocd/prereqs/

When using this pattern, add the Bitnami and Temporal chart repos to your AppProject sourceRepos list. Note that the prereq release names get an ArgoCD-applied prefix (e.g. plexicus-mongodb), so you must override global.required.database.host etc. to match the prefixed service names, or set fullnameOverride on each prereq Application.

Commit and apply:

cd plexicus-gitops
git add projects/plexicus-appproject.yaml apps/plexicus-app.yaml
git commit -m "feat(argocd): add plexicus AppProject + umbrella Application"
git push

kubectl apply -f projects/plexicus-appproject.yaml
kubectl apply -f apps/plexicus-app.yaml
5

Verify the sync

Open the ArgoCD UI or run:

argocd app get plexicus

The application should reach Healthy and Synced status within a few minutes. Confirm the platform is up with the same smoke test from step 9.


Next Steps

Once the platform is running, open https://plexicus.yourdomain.com in your browser to complete the initial account setup.

From there you can:

  • Connect your source code repositories from the Connectors section
  • Review advanced configuration options (resource limits, replica counts, storage classes) in the full values.yaml reference
  • Set up two-factor authentication for your account

For questions, access requests, or support, contact engineering@plexicus.ai.