Deployment (k8s + GitLab CI)¶
Production layout mirrors SoccerPredictAI: platform manifests (cae-platform) + app Helm chart (cae-copilot).
Domains¶
| Surface | URL |
|---|---|
| UI | https://cae-copilot.dmitryivanov.dev |
| API (Swagger) | https://api.cae-copilot.dmitryivanov.dev/docs |
| API base | https://api.cae-copilot.dmitryivanov.dev |
| Docs | https://docs.cae-copilot.dmitryivanov.dev (GitLab Pages) |
Namespaces¶
| Namespace | Contents |
|---|---|
cae-platform |
Qdrant (PVC, internal Service) |
cae-copilot |
FastAPI, Streamlit, Ingress, Secrets |
Secrets workflow¶
cp .env.example .env # fill CAEC_LLM_API_KEY etc.
make gen-secrets # .env → values-dev.yaml
make encrypt # SOPS encrypt .env + k8s secrets
git commit values-dev.enc.yaml .env.enc ...
Required GitLab CI variables (same server as soccer):
SERVER_SSH_HOSTNAME,SERVER_SSH_USER,SERVER_SSH_PORT,SERVER_SSH_PRIVATE_KEYSSH_KNOWN_HOSTS,AGE_KEY
CI/CD stages¶
- linting — ruff, helm lint
- test — pytest unit
- build — push
api,streamlitimages (:$CI_COMMIT_SHA) - deploy —
kubectl applyQdrant;helm upgradechart - deploy-images — bump image tags only
- pages — MkDocs → GitLab Pages
Deploy runs only on default branch, never from MR pipelines.
Qdrant storage (healserver)¶
The cluster has no default StorageClass. Qdrant uses storageClassName: manual
with a hostPath PV — same pattern as SoccerPredictAI (k8s/manifests/ns_ds/).
Before first apply:
sudo mkdir -p /mnt/nextcloud_data/cae_platform/qdrant
sudo chown -R 1000:1000 /mnt/nextcloud_data/cae_platform/qdrant
kubectl apply -f k8s/manifests/ns_cae-platform/
kubectl get pvc -n cae-platform # qdrant-data → Bound
kubectl get pods -n cae-platform # qdrant → Running
If Qdrant stays Pending, check: kubectl describe pod -n cae-platform -l app=qdrant
and kubectl describe pvc qdrant-data -n cae-platform.
Embedding model (offline prod)¶
The API image bakes intfloat/multilingual-e5-base into /app/models/e5-base
at Docker build time (scripts/bake_embedding_model.py). Pods set
CAEC_EMBEDDING_MODEL=/app/models/e5-base and HF_HUB_OFFLINE=1 — no HuggingFace
egress at runtime.
If ingest/RAG fails with No modules.json found or LocalEntryNotFoundError,
the running image is stale (hub cache only, not a saved bundle) — rebuild
image-api and redeploy.
Workaround before rebuild — ingest from a dev machine via port-forward (host has internet; cluster Qdrant is reached locally):
# terminal 1 — tunnel to healserver Qdrant
ssh -L 6333:127.0.0.1:6333 healserver \
"kubectl port-forward -n cae-platform svc/qdrant 6333:6333"
# terminal 2 — local mamba env (Rocky manifest ingest)
mamba activate cae-copilot
cp data/cae/ansys_rocky/v26r1/docs/manifest.example.yaml \
data/cae/ansys_rocky/v26r1/docs/manifest.yaml
CAEC_QDRANT_URL=http://127.0.0.1:6333 pdm run python scripts/ingest.py \
--manifest data/cae/ansys_rocky/v26r1/docs/manifest.yaml
First production deploy¶
# 1. Secrets
make gen-secrets && make encrypt && git push
# 2. GitLab pipeline on master builds images + deploys
# 3. Seed Qdrant (once per fresh PVC; requires API image with baked embeddings)
# Option A — Helm ingest Job (recommended; Rocky PDFs on kb-data volume):
kubectl delete job -n cae-copilot -l app.kubernetes.io/component=ingest 2>/dev/null || true
helm upgrade cae-copilot ~/k8s/helm/ns_cae-copilot -n cae-copilot \
-f ~/k8s/helm/ns_cae-copilot/values.yaml \
-f ~/k8s/helm/ns_cae-copilot/values-dev.yaml \
--set ingest.enabled=true --set ingest.runId=2 --wait
# Option B — one-off exec (manifest on mounted kb-data volume):
kubectl exec -n cae-copilot deploy/fastapi -- \
python scripts/ingest.py --manifest /app/data/cae/ansys_rocky/v26r1/docs/manifest.yaml
# 4. Smoke
SMOKE_BASE_URL=https://api.cae-copilot.dmitryivanov.dev make smoke
Local parity¶
make dev-stack
make ingest
make smoke
Manual Helm (on server)¶
helm upgrade --install cae-copilot ~/k8s/helm/ns_cae-copilot \
--namespace cae-copilot --create-namespace \
-f ~/k8s/helm/ns_cae-copilot/values.yaml \
-f ~/k8s/helm/ns_cae-copilot/values-dev.yaml
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
/ask returns 503 |
CAEC_DEMO_ENABLED=false |
Set to true in secrets / Helm values |
| RAG empty, low confidence | Qdrant not seeded | Run Helm ingest job; verify GET /kb/stats shows chunks > 0 |
No modules.json found |
API image missing baked embeddings | Rebuild and redeploy API image (see Embedding model) |
| Ingest job fails | Missing PDFs or manifest.yaml on kb-data volume |
Check kb.hostPath and Nextcloud sync path |
| Rate limit errors | Ingress or app middleware | Adjust nginx ingress annotations or CAEC_RATE_LIMIT_PER_MINUTE |