Skip to main content

Install on Kubernetes with Helm

Audience

Operators running Kubernetes who want to deploy TRUSCA with the production-grade Helm chart. Assume kubectl, Helm 3, and basic cluster administration (Ingress, StorageClasses, cert-manager) proficiency. If you run a single host, the Docker Compose install is simpler.

The Helm chart (charts/trustedoss, chart version 0.10.0) deploys the full portal: the FastAPI backend, the Celery worker and beat scheduler, the React frontend, an Ingress with TLS, and a database migration Job. PostgreSQL and Redis can either be bundled in-cluster (for evaluation) or pointed at external managed datastores (recommended for production).

Vulnerability matching ships in-chart

The worker pod ships with the Trivy DB and downloads / refreshes it from ghcr.io/aquasecurity/trivy-db (or a mirror via env.trivy.dbRepository). No external vulnerability engine is required. See Vulnerability data (Trivy DB).

What the chart deploys

WorkloadKindNotes
backendDeploymentFastAPI API. AUTO_MIGRATE=false — migrations run in the Job.
workerDeployment (+ optional HPA)Celery worker (cdxgen / scancode / Trivy).
beatDeployment (replicas: 1)Celery scheduler — singleton.
frontendDeploymentReact SPA on nginx (:8080).
postgresStatefulSetOptional bundle (postgres.bundled).
redisDeploymentOptional bundle (redis.bundled).
migrateJob (pre-install / pre-upgrade hook)alembic upgrade head as the owner role.
ingressIngresscert-manager TLS; API + SPA routing.

Prerequisites

  • A Kubernetes cluster and a kubectl context with permission to create the namespace and workloads.
  • Helm 3.
  • An ingress controller (the chart defaults to class nginx).
  • cert-manager with a ClusterIssuer named letsencrypt-prod for the default TLS configuration (override via ingress.annotations).
  • On multi-node clusters, a ReadWriteMany StorageClass for the shared scan workspace (workspace.persistence.storageClassName). A single-node cluster can use the per-pod emptyDir fallback.

Validate the chart before installing

Before deploying, render the in-repo chart locally to catch values / template errors without touching a cluster (Helm 3+, from the repository root):

SECRET=$(openssl rand -hex 32)
helm lint charts/trustedoss \
--set env.secret.secretKey="$SECRET" \
--set postgres.auth.password=throwaway \
--set ingress.host=trustedoss.example.com
helm template trustedoss charts/trustedoss --namespace trustedoss \
--set env.secret.secretKey="$SECRET" \
--set postgres.auth.password=throwaway \
--set ingress.host=trustedoss.example.com \
>/dev/null

helm lint reports chart-structure problems; helm template fully renders every manifest with the minimum required values, so a non-zero exit means the chart would not install. The --set values here are throwaway — the real install below uses your own secrets.

Quick start (bundled datastores, evaluation)

This runs PostgreSQL and Redis in-cluster — fast to stand up, but not recommended for production data.

helm install trustedoss oci://ghcr.io/trustedoss/charts/trustedoss \
--version 0.10.0 \
--namespace trustedoss --create-namespace \
--set env.secret.secretKey="$(openssl rand -hex 32)" \
--set postgres.auth.password="$(openssl rand -hex 24)" \
--set ingress.host=trustedoss.example.com \
--set env.corsAllowedOrigins=https://trustedoss.example.com

Replace trustedoss.example.com with your own hostname, and make sure DNS for that host points at your ingress controller.

Bundled datastores are for evaluation

The in-cluster PostgreSQL and Redis have modest defaults and a single replica. For anything beyond a trial, use external managed datastores (below).

Prefer Cloud SQL / RDS for PostgreSQL and Memorystore / ElastiCache for Redis over the in-cluster bundles. Provide a values file:

# values.prod.yaml
postgres:
bundled: false
redis:
bundled: false
env:
database:
url: postgresql+asyncpg://app:***@cloudsql-proxy:5432/trustedoss
# if you separate the DDL/owner role from the runtime role:
ownerUrl: postgresql+asyncpg://owner:***@cloudsql-proxy:5432/trustedoss
redis:
url: redis://memorystore:6379/0
secret:
# pre-created Secret carrying all four keys (see below)
existingSecret: trustedoss-prod-secrets
corsAllowedOrigins: https://trustedoss.example.com
ingress:
host: trustedoss.example.com

Then install:

helm install trustedoss oci://ghcr.io/trustedoss/charts/trustedoss \
--version 0.10.0 \
--namespace trustedoss --create-namespace \
-f values.prod.yaml
Secret contents are mandatory

When env.secret.existingSecret is set, the chart renders no Secret of its own. The referenced Secret must carry all four keys, or the pods will not start:

  • DATABASE_URL_APP
  • DATABASE_URL_OWNER
  • REDIS_URL
  • SECRET_KEY (at least 32 characters)
CORS in production

env.corsAllowedOrigins must enumerate the exact origins that serve the SPA — no wildcard in production. List every scheme + host that browsers will use.

How migrations run

A Helm pre-install + pre-upgrade hook Job runs alembic upgrade head once as the owner DB role (DATABASE_URL_OWNER). The application pods run with AUTO_MIGRATE=false, so the Job is the sole migrator.

Backend pods stay NotReady (/health/ready returns 503) until the schema is at HEAD, so traffic only ever reaches a migrated schema. Migrations are forward-only — the Job never downgrades. Hook ordering for the bundled case is: Secrets → Postgres Service / StatefulSet → migration Job, and the Job's init container waits for Postgres to accept connections before alembic runs.

Upgrade

helm upgrade trustedoss oci://ghcr.io/trustedoss/charts/trustedoss \
--version <new-chart-version> \
--namespace trustedoss \
-f values.prod.yaml

The pre-upgrade migration Job applies any new schema before the new pods roll out. Because migrations are forward-only, take a database backup before upgrading — see Backup & restore.

Key values

The full table lives in the chart README. The values you most often set:

KeyDefaultPurpose
image.tag0.10.0Image tag for backend / worker / frontend (never :latest).
ingress.host""Required. Public hostname.
env.corsAllowedOrigins""Required in prod. Allowed browser origins (no wildcard).
env.secret.secretKey""SECRET_KEY (≥32 chars). Required unless existingSecret.
env.secret.existingSecret""Pre-created Secret with all four keys; disables the chart Secret.
postgres.bundledtruefalse → use env.database.* (external).
redis.bundledtruefalse → use env.redis.url (external).
env.trivy.dbRepositoryghcr.io/aquasecurity/trivy-dbOverride for an air-gapped internal mirror — see Air-gapped operation.
env.trivy.dbRefreshHours168Weekly Trivy DB refresh; lower for fresher feeds.
worker.trivyDbPersistence.enabledtrueMount a PVC at /var/lib/trivy so the worker doesn't re-download on every restart.
workspace.persistence.storageClassName""RWX class for the shared scan volume on multi-node clusters.
worker.replicaCount2Prefer scaling worker pods over per-pod concurrency.

Verify it worked

  1. The migration Job completed:

    kubectl -n trustedoss get jobs
    # the trustedoss migrate Job should show COMPLETIONS 1/1
  1. All pods are Running and backend pods are Ready:

    kubectl -n trustedoss get pods
    # backend pods Ready means /health/ready returned 200 (schema at HEAD)
  1. The readiness probe passes from inside the cluster:

    kubectl -n trustedoss exec deploy/trustedoss-backend -- \
    curl -fsS http://localhost:8000/health/ready
    # → {"status":"ready"}
  1. The Ingress has an address and a valid certificate, then open https://<ingress.host>/ in a browser and sign in.

Troubleshooting

  • Backend pods stuck NotReady. /health/ready returns 503 until the schema is at HEAD. Check the migration Job logs:

    kubectl -n trustedoss logs job/trustedoss-migrate

    A failed Job usually means the owner DSN (DATABASE_URL_OWNER) lacks DDL privileges or cannot reach the database.

  • Pods CreateContainerConfigError with an existing Secret. The referenced Secret is missing one of the four required keys. Confirm:

    kubectl -n trustedoss get secret trustedoss-prod-secrets -o jsonpath='{.data}' | tr ',' '\n'
    # expect DATABASE_URL_APP, DATABASE_URL_OWNER, REDIS_URL, SECRET_KEY
  • Scans fail on multi-node clusters. The backend and worker share the scan workspace. Without a ReadWriteMany StorageClass the worker cannot read what the backend wrote. Set workspace.persistence.storageClassName to an RWX class (nfs / efs / filestore / longhorn).

  • TLS certificate never issues. The default annotations expect a cert-manager ClusterIssuer named letsencrypt-prod. Inspect the Certificate:

    kubectl -n trustedoss describe certificate

If you hit a chart bug, open an issue using the bug report template.

See also