# Deploy Helm Chart

Monolithic Helm chart for all backend services.

## Services

| Service              | Port(s)                 | Replicas | HPA      | Purpose                                     |
|----------------------|-------------------------|----------|----------|---------------------------------------------|
| `app`                | 3000 (HTTP)             | 2        | 2–10     | Main API server                             |
| `gitserver`          | 8021 (HTTP), 2222 (SSH) | 1        | 1–5      | Git HTTP + SSH server                       |
| `email_worker`       | 8084 (HTTP)             | 1        | disabled | Email queue consumer (single instance only) |
| `git_hook`           | 8083 (HTTP)             | 1        | 1–5      | Git hook worker pool                        |
| `metrics_aggregator` | 9090 (HTTP)             | 1        | 1–5      | Prometheus scrape + Loki push               |
| `static_server`      | 8081 (HTTP)             | 1        | 1–5      | Static file server (avatars, blobs, media)  |

## Prerequisites

The following resources must exist in the cluster **before** installing the Helm chart. They are not managed by Helm —
install, upgrade, and uninstall of the chart will not touch them.

### 1. Namespace

```bash
kubectl create namespace app
```

### 2. PVC (aliyun-nfs-app, 200Ti, ReadWriteMany)

```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-data
  namespace: app
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 200Ti
  storageClassName: aliyun-nfs-app
EOF
```

> The chart references this PVC by hardcoded name `shared-data`. This name is immutable — it cannot be changed via Helm
> values.

### 3. ConfigMap

```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-env
  namespace: app
data:
  APP_REPOS_ROOT: "/data/repos"
  APP_AVATAR_PATH: "/data/avatars"
  STORAGE_PATH: "/data/files"
  STATIC_ROOT: "/data"
  APP_LOG_LEVEL: "info"
  APP_COOKIE_SECURE: "false"
  APP_DOMAIN_URL: "https://your-domain.com"
  APP_DATABASE_URL: "postgres://user:pass@postgres:5432/app"
  APP_REDIS_URL: "redis://redis:6379"
  APP_AI_BASIC_URL: "https://api.openai.com/v1"
  APP_AI_API_KEY: "sk-..."
  APP_SMTP_PASSWORD: "..."
  APP_SESSION_SECRET: "min-32-byte-random-string..."
  APP_SSH_SERVER_PRIVATE_KEY: "<hex-encoded-private-key>"
EOF
```

| Variable                     | Default / Example           | Required  |
|------------------------------|-----------------------------|-----------|
| `APP_REPOS_ROOT`             | `/data/repos`               | Yes       |
| `APP_AVATAR_PATH`            | `/data/avatars`             | Yes       |
| `STORAGE_PATH`               | `/data/files`               | Yes       |
| `STATIC_ROOT`                | `/data`                     | Yes       |
| `APP_LOG_LEVEL`              | `info`                      | No        |
| `APP_COOKIE_SECURE`          | `false`                     | No        |
| `APP_DOMAIN_URL`             | `https://your-domain.com`   | Yes       |
| `APP_DATABASE_URL`           | `postgres://...`            | **Yes**   |
| `APP_REDIS_URL`              | `redis://...`               | **Yes**   |
| `APP_AI_BASIC_URL`           | `https://api.openai.com/v1` | **Yes**   |
| `APP_AI_API_KEY`             | `sk-...`                    | **Yes**   |
| `APP_SMTP_PASSWORD`          | `...`                       | **Yes**   |
| `APP_SESSION_SECRET`         | min 32 bytes                | **Yes**   |
| `APP_SSH_SERVER_PRIVATE_KEY` | hex-encoded PEM             | **Yes**   |
| `APP_SSH_PORT`               | `2222`                      | Yes (k8s) |

> **SSH host key**: `APP_SSH_SERVER_PRIVATE_KEY` must be the hex-encoded Ed25519 private key PEM bytes.
> ```bash
> ssh-keygen -t ed25519 -f /tmp/ssh_host_key -N ""
> hexdump -v -e '/1 "%02x"' < /tmp/ssh_host_key
> ```
>
> **Session secret**: generate 48 random bytes:
> ```bash
> openssl rand -base64 48
> ```
>
> Override the ConfigMap name with `--set configMapName=your-cm-name`.

### 4. Verify prerequisites

```bash
kubectl get namespace app
kubectl get pvc -n app shared-data
kubectl get configmap -n app app-env
```

## Quick Start

```bash
helm template deploy ./deploy --namespace app --set imageRegistry=ghcr.io/your-org
helm lint ./deploy

# Install
helm upgrade --install deploy ./deploy \
  --namespace app \
  --set imageRegistry=ghcr.io/your-org \
  --set imageTag=v0.2.9
```

## Storage

All services share a single PVC (`shared-data`) via `subPath` mounts:

| SubPath   | Mount           | Used By                  |
|-----------|-----------------|--------------------------|
| `repos`   | `/data/repos`   | app, gitserver, git-hook |
| `avatars` | `/data/avatars` | app                      |
| `files`   | `/data/files`   | app                      |
| `static`  | `/data`         | static-server            |

## Autoscaling

All services except `email_worker` have HPA enabled by default. The email worker is fixed at 1 replica and must not be
scaled.

To adjust HPA bounds per service:

```bash
--set services.app.autoscaling.maxReplicas=20
--set services.app.autoscaling.targetCPUUtilization=70
```

To disable HPA for a service:

```bash
--set services.git_hook.autoscaling.enabled=false
```

## Ingress

```bash
helm upgrade --install deploy ./deploy \
  --namespace app \
  --set ingress.enabled=true \
  --set ingress.className=nginx \
  --set ingress.hosts[0].host=your-domain.com
```

## Dependencies

All services require these to be reachable from the cluster:

- PostgreSQL (via `APP_DATABASE_URL`)
- Redis (via `APP_REDIS_URL`)
- Git binary (included in all Docker images)
- OpenAI-compatible API (via `APP_AI_BASIC_URL` + `APP_AI_API_KEY`)
- Qdrant vector DB (via `APP_QDRANT_URL`)
- SMTP server (via `APP_SMTP_*`)
- Embedding model (via `APP_EMBED_MODEL_*`)

Optional dependencies with graceful degradation:

| Dependency     | Variable                      | Fallback         |
|----------------|-------------------------------|------------------|
| NATS JetStream | `NATS_URL` + `NATS_TOKEN`     | Redis queue      |
| Loki           | `LOKI_URL`                    | Logs discarded   |
| OTEL Collector | `OTEL_EXPORTER_OTLP_ENDPOINT` | Tracing disabled |

## Production Example

```bash
helm upgrade --install deploy ./deploy \
  --namespace app \
  --set imageRegistry=ghcr.io/your-org \
  --set imageTag=v0.2.9 \
  --set services.app.replicas=3 \
  --set services.app.autoscaling.maxReplicas=20 \
  --set ingress.enabled=true \
  --set ingress.className=nginx \
  --set ingress.hosts[0].host=your-domain.com \
  --set configMapName=app-env
```
