# Self-hosting

See [Architecture](/self-hosting/architecture.md) for a high-level overview of the Scorable system.

## Scorable Installation Guide

This document walks you through deploying Scorable to your own Kubernetes cluster with our Helm chart.

{% hint style="info" %}
Self-hosting is available on the Scale plan. Contact Scorable at <hello@scorable.ai>
{% endhint %}

***

## Prerequisites

### Required infrastructure

| Component                      | Minimum                                     | Notes                                                                                                          |
| ------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Kubernetes                     | v1.30+                                      | Tested on AKS, GKE, and EKS                                                                                    |
| Helm                           | 3.14+                                       |                                                                                                                |
| PostgreSQL                     | 16, with the `pgvector` extension available | A managed service (Azure Database for PostgreSQL, AWS RDS, GCP Cloud SQL) or an in-cluster Postgres both work. |
| Redis                          | 7.x, TLS-capable                            | A managed service or in-cluster Redis.                                                                         |
| Container registry credentials | Scorable provides on onboarding             | Used to pull the Scorable container images.                                                                    |

### Optional but recommended

* **Ingress controller** (Traefik, NGINX, etc.) — required for hostname-based access.
* **cert-manager** — for automatic TLS certificate lifecycle. Works with any ACME issuer (Let's Encrypt, internal CA, etc.) or a `CA` issuer if you have your own root.
* **Sealed Secrets** or **External Secrets Operator (ESO)** — for production-grade secrets management. If you skip both, the chart accepts a plain Kubernetes `Secret` you create out-of-band.
* **Object storage** — Azure Blob Storage or AWS S3 for media uploads and Django static files. Without object storage, uploads and static assets do not persist across pod restarts.

***

## Installation Steps

### 1. Configure values

Create `my-values.yaml` based on the [example below](#example-values-file). The most commonly customised values are:

* `domain` — the hostname under which Scorable will be served.
* `hosts.postgres` / `hosts.redis` — DNS names of your data services.
* `frontend.{authUrl, apiBaseUrl, ...}` and `api.{frontendUrl, apiBaseUrl}` — your application's externally-visible URLs.
* `useAzureStorage` / `useS3` and the corresponding storage settings.
* `imagePullSecret` — name of the Kubernetes Secret holding your registry credentials.

### 2. Install the chart

The Helm chart and container images are both hosted on GitHub Container Registry. Scorable provides a GitHub PAT with `read:packages` scope on onboarding — the same token authenticates Helm and your in-cluster image-pull Secret.

```bash
# 1. Log in to ghcr.io.
echo "<gh-pat>" | helm registry login ghcr.io --username <gh-user> --password-stdin

# 2. Install
helm upgrade --install scorable oci://ghcr.io/root-signals/charts/scorable \
  --version <version> \
  --namespace scorable --create-namespace \
  -f my-values.yaml \
  --atomic --timeout 10m
```

`--atomic` rolls back the install if any pod fails to come up within the timeout.

### 3. Wait for pods

```bash
kubectl -n scorable get pods -w
```

All deployments (api, frontend, evals, taskiq, pgdog) should reach `Running 1/1`. The api container runs database migrations on startup.

***

## Database

Create the database and application role once, as your Postgres admin user, on the cluster that `hosts.postgres` points to.

```sql
CREATE ROLE scorable
  LOGIN
  PASSWORD 'REPLACE_WITH_STRONG_PASSWORD';

CREATE DATABASE scorable
  OWNER scorable
  ENCODING 'UTF8';

\c scorable

GRANT ALL ON SCHEMA public TO scorable;
CREATE SCHEMA llm_proxy AUTHORIZATION scorable;
```

If you change `postgresDb` or `postgresUser` in your values file, adjust the SQL above accordingly.

### Required PostgreSQL extensions

The application uses several Postgres extensions. On most managed Postgres offerings, only the superuser can create extensions, so create these once as the Postgres admin **before installing the chart**:

```sql
-- Run as Postgres admin, against the `scorable` database
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE EXTENSION IF NOT EXISTS btree_gist;
```

Some managed services additionally require extensions to be allow-listed at the server level before they can be created. Consult your provider's documentation if `CREATE EXTENSION` returns a "not allowed" error.

### Connection pooler (pgdog)

The chart ships pgdog as a subchart that pools connections in front of Postgres. If you override `postgresDb` or `hosts.postgres` at the top level of your values file, you must also override `pgdog.databases[0].name` and `pgdog.databases[0].host` to match — Helm cannot cross-reference between subchart blocks.

```yaml
postgresDb: "scorable"         # ← top-level setting …
hosts:
  postgres: "pg.example.com"   # ← and host
pgdog:
  databases:                   # ← must match here
    - name: scorable
      host: pg.example.com
      port: 5432
      role: primary
```

***

## Object storage

The chart supports Azure Blob Storage or AWS S3. Pick one; set `useAzureStorage: true` *or* `useS3: true`.

Two locations are needed: a publicly-readable one for Django static assets, and a private one for user uploads. Django rewrites static-file URLs to point directly at the object-storage endpoint, so the static-files location must permit anonymous reads — or be fronted by a CDN you trust to serve them.

For Azure: a single storage account with two containers (`public-data` set to blob-level anonymous access, `customer-data` private). For AWS: two S3 buckets (one with a static-website / public-read policy, one private). The chart authenticates to the private location with the credentials you supply in the secret.

***

## Secrets management

The chart needs a Kubernetes Secret available in the deployment namespace by the time pods start. There are three supported patterns.

### Option A — Plain Kubernetes Secret

Simplest, suitable for environments where Kubernetes RBAC is your sole secrets boundary.

```yaml
useSealedSecrets: false
secretRef: "rs-secrets"
```

Create the Secret yourself before installing the chart:

```bash
kubectl -n scorable create secret generic rs-secrets \
  --from-literal=POSTGRES_PASSWORD='...' \
  --from-literal=REDIS_PASSWORD='...' \
  --from-literal=REDIS_URL='rediss://:...?ssl_cert_reqs=none' \
  --from-literal=AZURE_STORAGE_CONNECTION_STRING='...' \
  --from-literal=SECRET_KEY='...' \
  --from-literal=NEXTAUTH_SECRET='...' \
  --from-literal=LLM_PROXY_MASTER_KEY='...' \
  --from-literal=LLM_PROXY_SALT_KEY='...' \
  --from-literal=OPENAI_API_KEY='sk-...' \
  --from-literal=AZURE_OPENAI_API_KEY='...'
```

`REDIS_URL` is optional. If unset, api/workers build the URL from the fragment-style `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` / `REDIS_SSL` env vars (set by the chart from `hosts.redis`, `ports.redis`, `redisSsl`, and the `REDIS_PASSWORD` secret). For managed Redis with TLS (Azure Managed Redis, Azure Cache for Redis Premium, ElastiCache with in-transit encryption), prefer setting `REDIS_URL` directly to avoid the fragment-based URL builder.

`SECRET_KEY`, `NEXTAUTH_SECRET`, `LLM_PROXY_MASTER_KEY`, and `LLM_PROXY_SALT_KEY` **must not be changed after first deploy** — they encrypt at-rest data. Generate them once and store them durably.

### Option B — Sealed Secrets

```yaml
useSealedSecrets: true
secrets:
  POSTGRES_PASSWORD: "AgAB..."          # encrypted with kubeseal
  # ... all the other keys, each encrypted
  PGDOG_USERS_TOML: "AgAB..."           # encrypted pgdog users config
```

Encrypt each value:

```bash
echo -n "your-secret-value" | kubeseal --scope cluster-wide --raw --from-file=/dev/stdin
```

### Option C — External Secrets Operator

If you have ESO installed and a `ClusterSecretStore` configured against your secret store (Azure Key Vault, AWS Secrets Manager, Vault, etc.):

```yaml
useSealedSecrets: false
secretRef: "rs-secrets"
```

Create an `ExternalSecret` resource yourself that materialises `rs-secrets` from your upstream store. The chart doesn't generate the `ExternalSecret` — it expects the `rs-secrets` Secret to exist when pods start.

### pgdog users secret

The chart's pgdog subchart needs a `pgdog-users` Secret with a `users.toml` payload. Provide its contents in `secrets.PGDOG_USERS_TOML`. With Sealed Secrets the value is encrypted in your values file. With plain Secret / ESO, the simplest approach is to pass the file at install time so the plaintext doesn't sit in your values file:

```bash
cat > /tmp/pgdog-users.toml <<EOF
[[users]]
name = "scorable"
database = "scorable"
password = "<same value as POSTGRES_PASSWORD>"
pool_size = 32
EOF

helm upgrade --install scorable ... --set-file secrets.PGDOG_USERS_TOML=/tmp/pgdog-users.toml
rm /tmp/pgdog-users.toml
```

***

## Ingress and TLS

The chart emits `Ingress` resources for `api` and `frontend` when `ingress.enabled: true` is set on each. Annotations and the `tls` block are fully values-driven — supply whatever your ingress controller and TLS strategy require.

### Example: Traefik + cert-manager + Let's Encrypt

```yaml
domain: "scorable.example.com"

api:
  frontendUrl: "https://scorable.example.com"
  apiBaseUrl:  "https://api.scorable.example.com"
  ingress:
    enabled: true
    className: traefik
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt"
      external-dns.alpha.kubernetes.io/hostname: "api.scorable.example.com"
    tls:
      enabled: true

frontend:
  authUrl:           "https://scorable.example.com"
  authUrlInternal:   "http://127.0.0.1:3000"
  apiBaseUrl:        "https://api.scorable.example.com"
  apiBaseUrlServer:  "http://api:80"
  ingress:
    enabled: true
    className: traefik
    host: "scorable.example.com"
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt"
      external-dns.alpha.kubernetes.io/hostname: "scorable.example.com"
    tls:
      enabled: true
```

Notes:

* `frontend.ingress.host` defaults to `.Values.domain` (the apex). If you prefer the frontend at `app.<domain>` while the API stays at `api.<domain>`, set `frontend.ingress.host: "app.<domain>"` explicitly.
* `api.ingress.host` defaults to `api.<.Values.domain>`.
* The four `frontend.{authUrl, authUrlInternal, apiBaseUrl, apiBaseUrlServer}` and `api.{frontendUrl, apiBaseUrl}` values **must** match the URLs your users will type. The chart's defaults are `http://localhost:*`, intended for `kubectl port-forward` development.

### Example: WAF or LB in front, chart ingress controller behind

Common enterprise pattern. A WAF/LB layer (e.g. Azure Application Gateway, AWS ALB) terminates TLS in front; the chart's ingress controller serves plain HTTP behind it.

```yaml
api:
  ingress:
    enabled: true
    className: traefik
    annotations:
      external-dns.alpha.kubernetes.io/hostname: "api.scorable.example.com"
    tls:
      enabled: false       # WAF terminates TLS

frontend:
  ingress:
    enabled: true
    className: traefik
    annotations:
      external-dns.alpha.kubernetes.io/hostname: "scorable.example.com"
    tls:
      enabled: false
```

***

## Example values file

A complete-ish example covering common knobs. Adjust to your environment.

```yaml
domain: "scorable.example.com"
environment: "production"

# Storage — pick one (Azure)
useAzureStorage: true
useS3: false
azureStorageAccountName: "scorablestorageabc123"
azureStoragePublicDataContainer: "public-data"
azureStorageCustomerDataContainer: "customer-data"

# Secrets
useSealedSecrets: false
secretRef: "rs-secrets"
imagePullSecret: "scorable-image-pull-secret"

# Hosts
hosts:
  postgres: "pg.scorable.example.com"
  redis:    "redis.scorable.example.com"

postgresUser: "scorable"
postgresDb:   "scorable"
redisSsl:     true

# Connection pooler — must match postgresDb + hosts.postgres
pgdog:
  enabled: true
  databases:
    - name: scorable
      host: pg.scorable.example.com
      port: 5432
      role: primary

# Application URLs — override when using Ingress
frontend:
  authUrl:           "https://scorable.example.com"
  authUrlInternal:   "http://127.0.0.1:3000"
  apiBaseUrl:        "https://api.scorable.example.com"
  apiBaseUrlServer:  "http://api:80"
  ingress:
    enabled: true
    className: traefik
    host: "scorable.example.com"
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt"
    tls:
      enabled: true

api:
  frontendUrl: "https://scorable.example.com"
  apiBaseUrl:  "https://api.scorable.example.com"
  ingress:
    enabled: true
    className: traefik
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt"
    tls:
      enabled: true

# LLM proxy — enable when you have a real LLM provider configured
llmProxy:
  enabled: false

# RAG — optional
rag:
  enabled: false

# Optional: include additional resources alongside the chart
extraManifests: []
```

***

## Accessing the application

### Using Ingress (production)

With Ingress configured per the example above, the app is at `https://<domain>` and the API at `https://api.<domain>`. Create an admin user, then sign in.

### Creating the first admin user

```bash
POD=$(kubectl -n scorable get pods -l app=api --no-headers -o custom-columns=:metadata.name | head -1)
kubectl -n scorable exec "$POD" -- \
  env DJANGO_SUPERUSER_USERNAME=admin \
      DJANGO_SUPERUSER_EMAIL=admin@example.com \
      DJANGO_SUPERUSER_PASSWORD='ChangeMe123!' \
  python manage.py createsuperuser --noinput
```

Then visit `https://api.<domain>/admin/` to log into Django admin, or `https://<domain>` to log into the frontend.

### Using port forwarding (development)

```bash
kubectl port-forward --namespace scorable service/frontend 3000:80 &
kubectl port-forward --namespace scorable service/api 8000:80 &
```

Open `http://localhost:3000`. With this access path, leave the chart's default `frontend.authUrl` / `frontend.apiBaseUrl` values as-is — they target `localhost`.

***

## Monitoring and Logging

### Sentry

Set `api.sentry.dsn`, `frontend.sentry.dsn`, `evals.sentry.dsn`, etc. in your values file. The chart wires DSNs into pod environments.

### Prometheus

The application exposes metrics at `/metrics` endpoints. Configure your Prometheus instance to scrape:

* `api:80/metrics`
* `frontend:80/metrics`
* `evals:80/metrics`
* `pgdog:9090/metrics`

For detailed assistance, contact our support team.

***

## Updates

Scorable notifies customer contact persons when new versions of the Helm chart are released.

```bash
helm upgrade scorable oci://ghcr.io/root-signals/charts/scorable \
  --version <new-version> \
  --namespace scorable \
  -f my-values.yaml \
  --atomic --timeout 10m
```

Always read the release notes for breaking changes before upgrading.

***

## Support

* **Email**: `support@scorable.ai`
* **Slack** (provided on onboarding)

Keep your Kubernetes cluster, managed-service versions, and Helm up to date. Back up your database and configurations regularly.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.scorable.ai/self-hosting.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
