ClouisleClouisle

Kubernetes deployment

Run Clouisle in Kubernetes with Helm or a single-file manifest

A Kubernetes deployment requires Kubernetes 1.25+, a configured kubectl, an Ingress Controller, and access to an image registry; the Helm approach requires Helm 3. Scaling API replicas with local upload storage requires a StorageClass that supports ReadWriteMany.

External service requirements: PostgreSQL 17+ with pg_search (0.24.3) and pg_stat_statements preloaded, Redis 6+, Qdrant 1.7+.

Storage and default replicas

The single-file manifest creates three PVCs by default: postgres-data (10Gi, RWO), qdrant-data (10Gi, RWO), and uploads-data (10Gi, RWX, mounted by the API at /app/uploads only). Redis persistence differs: the single-file manifest uses emptyDir (cache recoverable), while Helm defaults to a 5Gi PVC.

Default replicas: api 2, worker 2, sandbox-worker 1, beat 1 (Recreate strategy), frontend 2. The manifest has no HPA; replicas are static and adjusted with kubectl scale.

ConfigMap and Secret

The single-file manifest lays resources out in this order: Namespace → ConfigMap → Secret → PostgreSQL (StatefulSet + headless Service + PVC) → Redis (Deployment + Service) → Qdrant (StatefulSet + headless Service + PVC) → uploads PVC → API → Worker → Sandbox Worker → Beat → Frontend → Ingress — 13 sections.

Every key in clouisle-config must match the manifest (pods inject the whole ConfigMap with envFrom.configMapRef):

KeyValuePurpose
PROJECT_NAME / TIMEZONEClouisle / Asia/ShanghaiApplication name and timezone
API_BASE_URL / API_INTERNAL_BASE_URLhttp://api:8000Internal address for server-side code and workers
PUBLIC_API_URL""Set when you deploy behind a public domain
SANDBOX_ARTIFACT_UPLOAD_BASE_URLhttp://api:8000Sandbox artifact uploads
SANDBOX_WORKER_CONCURRENCY"1"Sandbox worker concurrency (consumed by ${SANDBOX_WORKER_CONCURRENCY:-1} in the command)
SANDBOX_FILESYSTEM_ISOLATION_ENABLED"true"Turn on bwrap filesystem isolation
SANDBOX_FILESYSTEM_ISOLATION_BINARY"/usr/bin/bwrap"Absolute bubblewrap path inside the image
FRONTEND_URLhttp://frontend:3000SSO redirect base
POSTGRES_SERVER / POSTGRES_PORT / POSTGRES_USER / POSTGRES_DBpostgres / 5432 / postgres / clouisleDatabase (password lives in the Secret)
REDIS_HOST / REDIS_PORTredis / 6379Redis
VECTOR_BACKEND / QDRANT_URL / QDRANT_COLLECTION_PREFIX / QDRANT_DISTANCEqdrant / http://qdrant:6333 / kb_dim / CosineVector store
RETRIEVAL_HYBRID_KILL_SWITCH / RETRIEVAL_SHADOW_ENABLED"false" / "false"Retrieval switches
BACKEND_CORS_ORIGINS'["http://localhost:3000"]'Replace with your real frontend origin

clouisle-secret holds 6 keys that must be replaced: SECRET_KEY, POSTGRES_PASSWORD, REDIS_PASSWORD, QDRANT_API_KEY, INTERNAL_API_TOKEN, SANDBOX_ARTIFACT_UPLOAD_API_KEY. The template values are Base64 placeholders; when editing by hand, generate them with echo -n 'value' | base64.

API, Worker, Sandbox Worker, and Beat all carry a wait-for-postgres initContainer that blocks on pg_isready. The API readiness/liveness probes are GET /api/v1/health:8000 (initialDelaySeconds 10/20) and the frontend probes are GET /:3000. Worker and sandbox-worker read the internal token through INTERNAL_API_TOKEN_FILE=/var/run/secrets/clouisle/internal-api-token (mounted from the Secret) and set UPLOAD_STORAGE_MODE=remote.

The frontend Deployment in the single-file manifest (and the Helm defaults) sets no environment variables at all. Browser traffic still works because the Ingress routes the /api prefix, but Next.js server-side fetching falls back to the code default BACKEND_INTERNAL_URL=http://localhost:8000, which is unreachable inside the pod. Add BACKEND_INTERNAL_URL=http://api:8000 to the frontend workload if you rely on SSR fetching.

The Ingress uses the nginx class with the annotations proxy-body-size: 100m, proxy-read-timeout: 1800, proxy-send-timeout: 1800, proxy-http-version: 1.1, and use-forwarded-headers: true; path /api → api:8000 and / → frontend:3000. The default host is clouisle.example.com and must be replaced with your real domain.

helm lint deploy/helm/clouisle \
  --set-string secrets.values.INTERNAL_API_TOKEN=lint-only-token
helm upgrade --install clouisle deploy/helm/clouisle \
  --namespace clouisle --create-namespace \
  --set-string secrets.values.INTERNAL_API_TOKEN="$(openssl rand -hex 32)"

In production, put SECRET_KEY, database, Redis, Qdrant, Sandbox, and internal Tokens into Kubernetes Secrets and use values-production.yaml.

Generate a single-file manifest

curl -fsSL https://raw.githubusercontent.com/clouisle/Clouisle/main/deploy/install.sh | \
  CLOUISLE_DEPLOYMENT=k8s CLOUISLE_K8S_MANIFEST="$PWD/clouisle-k8s.yaml" \
  CLOUISLE_YES=1 bash
kubectl apply -f ./clouisle-k8s.yaml

The generated file has 0600 permissions and contains Base64-encoded Secrets; the installer neither applies automatically nor modifies the template. Review images, domains, Ingress, storage, and Secrets before applying.

Scaling and limits

kubectl -n clouisle scale deployment worker --replicas=4
kubectl -n clouisle scale deployment sandbox-worker --replicas=2
kubectl -n clouisle scale deployment api --replicas=3

beat always stays at one replica. With ReadWriteOnce local upload volumes, keep the API at a single replica; production with multiple replicas should configure a shared volume or object storage.

The worker consumes the default,agent,knowledge,workflow queues (Helm worker.command), so scaling the deployment grows all four task classes together. To grow one class only, add a worker deployment whose command ends with a single queue (for example -Q agent); the sandbox queue is consumed by the separate sandbox-worker deployment. Queue-to-task mapping: Deployment architecture → Celery queue layout.

Kubernetes resources and Ingress
Kubernetes resources and Ingress

How is this guide?

On this page