# 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](https://docs.syntho.ai/deploy-syntho/deploy-syntho-using-kubernetes/prerequisites) 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](https://docs.syntho.ai/deploy-syntho/deploy-syntho-using-kubernetes/back-up-postgresql).
{% 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](https://docs.syntho.ai/deploy-syntho/deploy-syntho-using-kubernetes/troubleshooting).
{% endstep %}

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

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

### Next steps

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