# 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: "" 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 | Pods run as UID/GID `1000` and set `fsGroup: 1000` so Git processes can create temporary object directories under bare repositories. If an existing PVC was previously written by another UID, fix ownership once from a maintenance pod: ```bash chown -R 1000:1000 /data/repos chmod -R u+rwX,g+rwX /data/repos ``` ## 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 ```