# Kubernetes (Helm)

Deploy Syntho on Kubernetes using Helm charts.

<details>

<summary>What Helm deploys</summary>

This deployment typically includes:

* Frontend (UI)
* Backend
* Core API
* Ray (separate chart)
* PostgreSQL (metadata, optional if you use an external DB)
* Redis (optional if you use an external Redis)

</details>

<details>

<summary>Ray sizing and storage (optional)</summary>

Ray capacity depends on your data size and throughput goals. If you didn’t get sizing guidance yet, ask Syntho Support.

Also ensure your cluster can provision **shared (RWX) storage** for Ray workers when the chart enables shared volumes.

If your StorageClass does not support `ReadWriteMany`, Ray pods can fail to schedule or start.

OpenShift / CRI-O note: some clusters default to low process limits. Ray can hit those limits when scaling.

</details>

<details>

<summary>Offline deployment</summary>

Offline Kubernetes deployments follow the same steps below. You just need to stage artifacts on a connected machine first.

1. **Stage Helm charts**
   * Download the Helm chart bundles (or clone `deployment-tools`).
   * Transfer the chart bundles to the offline environment where you run `helm upgrade --install`.
2. **Stage container images**

   Your cluster nodes must be able to fetch images.

   * **Recommended:** mirror images into an internal registry reachable by the cluster nodes.
   * **Alternative:** preload images onto every node (works, but is harder to operate).

   After mirroring, update image references in both `helm/ray/values.yaml` and `helm/syntho-ui/values.yaml` to point to your internal registry (and keep using a specific version tag).
3. **Registry authentication step changes**
   * If you use an **internal registry**, create the ImagePullSecret for that registry and reference it via `imagePullSecrets`.
   * If you **preload images** on nodes, ensure the chart uses an `imagePullPolicy` that doesn’t force pulls (commonly `IfNotPresent`).

{% hint style="info" %}
Upgrades are the same procedure. Repeat the staging step for the new `APPLICATION_VERSION`.
{% endhint %}

</details>

### Deploy

{% stepper %}
{% step %}
**Prerequisites**

Make sure you meet the [prerequisites](/deploy-syntho/deploy-syntho-using-kubernetes/prerequisites.md) before deploying.
{% endstep %}

{% step %}
**Get the Helm charts**

Use the `deployment-tools` repository:

* [Ray Helm chart](https://github.com/syntho-ai/deployment-tools/tree/main/helm/ray)
* [Syntho Helm chart](https://github.com/syntho-ai/deployment-tools/tree/main/helm/syntho-ui)

If you prefer release artifacts, use the `deployment-tools` releases matching the version that you are deploying:

* <https://github.com/syntho-ai/deployment-tools/releases>

Download:

* `ray-helm-chart.tar.gz`
* `syntho-ui-helm-chart.tar.gz`
  {% endstep %}

{% step %}
**Create a namespace**

```bash
kubectl create namespace syntho
```

{% endstep %}

{% step %}
**Create an ImagePullSecret**

```bash
kubectl create secret docker-registry syntho-cr-secret \
  --namespace syntho \
  --docker-server=<REGISTRY_HOST> \
  --docker-username=<USERNAME> \
  --docker-password=<PASSWORD>
```

In both charts, reference it:

```yaml
imagePullSecrets:
  - name: syntho-cr-secret
```

{% hint style="info" %}
Ensure that `syntho.azurecr.io` is added to your firewall allowlist to enable image pulls
{% endhint %}
{% endstep %}

{% step %}
**Deploy Ray**

Ray provides distributed execution for heavy jobs.

Minimal settings in your Ray values file (commonly `helm/ray/values.yaml`):

Use a specific `<version-number>` for production. Avoid `latest` unless Syntho tells you to.

```yaml
SynthoLicense: <license-key>

kuberay-operator:
  imagePullSecrets:
    - name: syntho-cr-secret

ray-operator:
  imagePullSecrets:
    - name: syntho-cr-secret

ray-cluster:
  imagePullSecrets:
    - name: syntho-cr-secret
  image:
    tag: <version-number>
```

Deploy:

```bash
helm upgrade --install ray-cluster ./helm/ray/chart \
  --values helm/ray/values.yaml \
  --namespace syntho
```

{% hint style="info" %}
If your bundle uses a different values filename or path, use that path in `--values`.
{% endhint %}

{% hint style="info" %}
The Ray head service is typically `ray-cluster-ray-head`. Use it as `ray_address` in the Syntho chart.
{% endhint %}

<details>

<summary>Ray troubleshooting (ArgoCD, permissions)</summary>

If Ray pods fail due to volume permissions, set a permissive security context:

```yaml
ray-cluster:
  head:
    securityContext:
      runAsUser: 0
      runAsGroup: 0
  worker:
    securityContext:
      runAsUser: 0
      runAsGroup: 0
```

</details>
{% endstep %}

{% step %}
**Deploy Syntho**

Edit your syntho-ui values file (commonly `helm/syntho-ui/values.yaml`).

{% hint style="info" %}
Production recommendation: use a hosted / managed PostgreSQL.

See [Back up PostgreSQL](/deploy-syntho/deploy-syntho-using-kubernetes/back-up-postgresql.md).
{% endhint %}

**Set images**

Set image repositories and tags to the versions provided by Syntho.

Use a specific `<version-number>` for production. Avoid `latest` unless Syntho tells you to.

```yaml
frontend:
  image:
    repository: synthoregistry.azurecr.io/syntho-core-frontend
    tag: <version-number>

backend:
  image:
    repository: synthoregistry.azurecr.io/syntho-core-backend
    tag: <version-number>

core:
  image:
    repository: synthoregistry.azurecr.io/syntho-core-api
    tag: <version-number>
```

**Set license key**

Set the license key in the Syntho chart values:

```yaml
SynthoLicense: <license-key>
```

Use the same license key value as in `helm/ray/values.yaml`.

**Configure external PostgreSQL (recommended)**

<details>

<summary>Example values</summary>

Create an external PostgreSQL host and two databases (common setup):

* Backend metadata DB
* Core API metadata DB

Then configure the Syntho Helm values to use them and disable the embedded databases.

Common values:

```yaml
backend:
  database_enabled: false
  db:
    host: <pg-host>
    port: 5432
    user: <user>
    password: <password>
    name: <backend-db>

core:
  database_enabled: false
  db:
    host: <pg-host>
    port: 5432
    username: <user>
    password: <password>
    name: <core-db>
```

</details>

**Configure Redis**

By default, the chart deploys Redis and exposes it as `redis-svc`.

If you use an external Redis, update these values:

```yaml
backend:
  redis:
    host: redis-svc
    port: 6379
    db: 0

core:
  redis:
    host: redis-svc
    port: 6379
    db: 1
```

<details>

<summary>Using the chart-managed PostgreSQL/Redis (optional)</summary>

Keep the embedded dependencies enabled if you don’t run external services.

Disable embedded PostgreSQL by setting `database_enabled: false` (as shown above).

Minimal example:

```yaml
backend:
  database_enabled: true
  db:
    host: database
    port: 5432

core:
  database_enabled: true
  db:
    host: database
    port: 5432
```

If you keep embedded PostgreSQL enabled, the in-cluster service is typically:

* Host: `database`
* Port: `5432`

</details>

Frontend URL and Ingress:

```yaml
frontend_url: <hostname>
frontend_protocol: https # or http

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: <hostname>
      paths:
        - path: /
          pathType: Prefix
```

The Ingress also routes backend traffic using path-based routing (typically `GET /api/*`).

You don’t need a separate Ingress for the backend.

<details>

<summary>Ingress annotations and TLS (nginx + cert-manager example)</summary>

```yaml
ingress:
  enabled: true
  name: frontend-ingress
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: "" # when using cert-manager
    nginx.ingress.kubernetes.io/proxy-buffer-size: "32k"
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-body-size: "512m"
  hosts:
    - host: <hostname>
      paths:
        - path: /
          pathType: Prefix
  tls: # remove this section when not using TLS
    - hosts:
        - <hostname>
      secretName: frontend-tls
```

</details>

Admin user:

```yaml
backend:
  user:
    username: admin
    email: admin@company.com
    password: <password>
```

Backend secret key:

```yaml
backend:
  secret_key: <random-string>
```

Core API encryption key:

```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```

```yaml
core:
  secret_key: <fernet-key>
```

Ray address:

```yaml
ray_address: ray-cluster-ray-head
```

<details>

<summary>Use externally managed Kubernetes Secrets (optional)</summary>

By default, the chart creates `backend-secret` and `core-secret` from your Helm values.

If you want to bring your own Secrets (for example via an external secrets manager), set:

* `backend.manualSecretName: <your-secret-name>`
* `core.manualSecretName: <your-secret-name>`

Create the Secrets in the same namespace.

Backend Secret must include:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: <backend-manual-secret-name>
type: Opaque
stringData:
  backend.db.password: "<db-password>"
  backend.secret_key: "<backend-secret-key>"
  backend.user.password: "<admin-password>"
```

Core Secret must include:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: <core-manual-secret-name>
type: Opaque
stringData:
  core.secret_key: "<fernet-key>"
  core.database_url: "postgresql+asyncpg://<username>:<password>@<host>:5432/<db>"
  license_key: "<license-key>"
```

</details>

Deploy:

```bash
helm upgrade --install syntho-ui ./helm/syntho-ui \
  --values helm/syntho-ui/values.yaml \
  --namespace syntho
```

{% hint style="info" %}
If your bundle uses a different values filename or path, use that path in `--values`.
{% endhint %}

<details>

<summary>HTTPS and cookies</summary>

If TLS is terminated at a reverse proxy / load balancer / Ingress, Syntho must be configured for `https`.

If Syntho is configured for `http`, browsers can reject cookies.

Symptoms include login loops or sessions not sticking.

Fix:

* Set the frontend protocol to `https`.
* Disable secure cookies.

</details>
{% endstep %}

{% step %}
**Verify deployment**

Check pods:

```bash
kubectl get pods -n syntho
```

Open the UI at your `frontend_url`.

If pods are not Ready, check logs:

```bash
kubectl logs -n syntho <pod-name>
```

{% hint style="info" %}
If the UI doesn’t resolve, check DNS and the Ingress external address first.
{% endhint %}

If this does not work, see [Troubleshooting](/deploy-syntho/deploy-syntho-using-kubernetes/troubleshooting.md).
{% endstep %}

{% step %}
**Back up PostgreSQL**

[Back up PostgreSQL](/deploy-syntho/deploy-syntho-using-kubernetes/back-up-postgresql.md) Syntho application databases before first use to validate the process.
{% endstep %}
{% endstepper %}

### Next steps

* Day-to-day commands: [Operations](/deploy-syntho/deploy-syntho-using-kubernetes/operations.md)
* Upgrade procedure: [Upgrade](/deploy-syntho/deploy-syntho-using-kubernetes/upgrade.md)
* Common issues: [Troubleshooting](/deploy-syntho/deploy-syntho-using-kubernetes/troubleshooting.md)


---

# 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.syntho.ai/deploy-syntho/deploy-syntho-using-kubernetes.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.
