Deployment ~10 min
This guide covers deploying every part of the VENI-AI platform: Shell API, Shell UI, the docs site, and integrated app services.
Quick deploy
./scripts/deploy.sh {env} {tag} [--kubeconfig path]| Argument | Example | Notes |
|---|---|---|
env | dev, uat, prod | Selects kustomize overlay |
tag | dev-abc1234 | Image tag ({env}-{gitsha} for dev) |
--kubeconfig | (auto-detected) | Defaults to ../../cluster/{env}.yaml |
# Deploy latest commit to dev
./scripts/deploy.sh dev dev-$(git rev-parse --short HEAD)
# Deploy a release candidate to UAT
./scripts/deploy.sh uat v1.2.0-rc.1
# Deploy to production
./scripts/deploy.sh prod v1.2.0 --kubeconfig ~/cluster/prod.yamlThe script updates image tags in infrastructure/k8s/overlays/{env}/kustomization.yaml using kustomize edit set image, then runs kubectl apply -k.
Services overview
| Service | Type | Image | Port |
|---|---|---|---|
shell-api | Bun + Hono API | registry.venizia.ai/shell-api:{tag} | 3000 |
shell-ui | React + Nginx | registry.venizia.ai/shell-ui:{tag} | 80 |
docs | VitePress + Nginx | registry.venizia.ai/docs:{tag} | 80 |
drive-api | Bun + Hono | registry.venizia.ai/drive-api:{tag} | 3001 |
document-api | Bun + Hono | registry.venizia.ai/document-api:{tag} | 3002 |
auto-report-api | Bun + Hono | registry.venizia.ai/auto-report-api:{tag} | 3003 |
hrm-api | Bun + Hono | registry.venizia.ai/hrm-api:{tag} | 3004 |
ai-assistant-api | Bun + Hono | registry.venizia.ai/ai-assistant-api:{tag} | 3005 |
Namespaces and image tags
| Env | Namespace | Tag format | Example |
|---|---|---|---|
| dev | veni-dev | dev-{gitsha} | dev-abc1234 |
| uat | veni-uat | {semver}-rc.{n} | v1.2.0-rc.1 |
| prod | veni-prod | {semver} | v1.2.0 |
Kustomize overlay structure
infrastructure/k8s/
├── base/ Base manifests (Deployment, Service, ConfigMap, Ingress)
│ ├── shell-api/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ ├── shell-ui/
│ ├── docs/
│ ├── drive-api/
│ ├── document-api/
│ ├── auto-report-api/
│ ├── hrm-api/
│ └── ai-assistant-api/
└── overlays/
├── dev/
│ ├── kustomization.yaml Image tags + resource patches
│ ├── resources-patch.yaml Env vars, resource limits
│ └── secrets.env Secrets (gitignored)
├── uat/
└── prod/Adding a new service to an overlay
Edit infrastructure/k8s/overlays/{env}/kustomization.yaml and add the service under resources and images:
resources:
- ../../base/shell-api
- ../../base/shell-ui
- ../../base/docs
- ../../base/drive-api # ← add new service here
images:
- name: registry.venizia.ai/drive-api
newTag: dev-abc1234Environment variables per service
Shell API
These vars differ from local dev
| Variable | K8s value | Why |
|---|---|---|
APP_ENV_LOG_PATH | /tmp | readOnlyRootFilesystem: true on the container |
APP_ENV_KEYCLOAK_URL | https://auth.{env}.venizia.ai | Public URL — browser must reach it |
APP_ENV_KEYCLOAK_INTERNAL_URL | http://keycloak:8080 | Internal URL — server token exchange |
APP_ENV_REDIS_URL | redis://:password@redis:6379 | Redis requires auth in K8s |
APP_ENV_DATABASE_URL | postgresql://shell:pass@postgres:5432/shell_db | Internal service hostname |
Shell UI
| Variable | Dev | Prod |
|---|---|---|
VITE_API_URL | http://localhost:3000/api | https://api.{env}.venizia.ai |
VITE_DOCS_URL | http://localhost:5177 | https://docs.venizia.ai |
VITE_* are build-time
VITE_* variables are baked into the JS bundle at build time, not injected at runtime. Set them before running docker build or in your CI pipeline.
Integrated apps
Each app service reads its own env block. Common pattern:
# overlays/dev/resources-patch.yaml
- op: add
path: /spec/template/spec/containers/0/env
value:
- name: DATABASE_URL
value: postgresql://drive:pass@postgres:5432/drive_db
- name: STORAGE_BUCKET
value: veni-drive-dev
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: veni-secrets
key: JWT_SECRETBuilding images
Shell API
cd shell/api
docker build -t registry.venizia.ai/shell-api:dev-$(git rev-parse --short HEAD) .
docker push registry.venizia.ai/shell-api:dev-$(git rev-parse --short HEAD)Shell UI
cd shell/ui
# Inject build-time env vars
VITE_API_URL=https://api.dev.venizia.ai \
VITE_DOCS_URL=https://docs.venizia.ai \
docker build -t registry.venizia.ai/shell-ui:dev-$(git rev-parse --short HEAD) .
docker push registry.venizia.ai/shell-ui:dev-$(git rev-parse --short HEAD)Docs site
cd docs
npm run build # Outputs to docs/.vitepress/dist/
docker build -t registry.venizia.ai/docs:dev-$(git rev-parse --short HEAD) .
docker push registry.venizia.ai/docs:dev-$(git rev-parse --short HEAD)The docs Dockerfile serves the static dist/ output with Nginx. A minimal example:
FROM node:22-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine
COPY --from=build /app/.vitepress/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80CI/CD pipeline
The GitHub Actions workflow (.github/workflows/ci-cd.yml) follows this flow:
push to develop ──► build & test ──► push :dev-{sha} ──► deploy to veni-dev
tag v*.*.*-rc.* ──► build & test ──► push :{tag} ──► deploy to veni-uat
tag v*.*.* ────────► build & test ──► push :{tag} ──► deploy to veni-prod (manual approval)Secrets required in GitHub repo settings
| Secret | Description |
|---|---|
REGISTRY_USERNAME | Container registry login |
REGISTRY_PASSWORD | Container registry password |
KUBECONFIG_DEV | Base64-encoded kubeconfig for dev cluster |
KUBECONFIG_UAT | Base64-encoded kubeconfig for UAT cluster |
KUBECONFIG_PROD | Base64-encoded kubeconfig for prod cluster |
Deploy checklist
Before deploying to prod, verify:
- [ ] All tests pass in CI (
npm test) - [ ] Lint clean (
npm run lint) - [ ] DB migrations applied (
bun run migrate) - [ ] Secrets updated in
veni-prodnamespace - [ ]
VITE_API_URLandVITE_DOCS_URLset correctly for the target env - [ ] Rollback plan documented (previous tag noted)
- [ ] Staging (UAT) tested and sign-off received
Rollback
# Undo the last deploy for all core services
kubectl rollout undo deployment/shell-api -n veni-dev
kubectl rollout undo deployment/shell-ui -n veni-dev
# Check status
kubectl rollout status deployment/shell-api -n veni-dev
kubectl get pods -n veni-dev
# Rollback a specific integrated app
kubectl rollout undo deployment/drive-api -n veni-devTo roll back to a specific revision:
# List revision history
kubectl rollout history deployment/shell-api -n veni-prod
# Roll back to revision 3
kubectl rollout undo deployment/shell-api -n veni-prod --to-revision=3Database migrations
Run migrations before deploying a new version that requires schema changes:
# Connect to the cluster's DB via a temporary pod
kubectl run migrate --rm -it \
--image=registry.venizia.ai/shell-api:v1.2.0 \
--env="DATABASE_URL=postgresql://..." \
-n veni-prod \
-- bun run migrateOr use a Kubernetes Job manifest in infrastructure/k8s/jobs/migrate.yaml.
Common issues
ImagePullBackOff CI hasn't finished building and pushing the image yet. The previous pods keep running — no downtime. Wait for CI to complete, or re-run the deploy script once the image is available.
CrashLoopBackOff immediately at startup Check logs: kubectl logs -n veni-dev <pod> --previous Common causes:
readOnlyRootFilesystem+ writing to a non-/tmppath → setAPP_ENV_LOG_PATH=/tmprbac_model.confnot found → ensureconfig/rbac_model.confexists and Dockerfile copies it- Redis
NOAUTH→ password missing inAPP_ENV_REDIS_URL - DB hostname not found → verify
APP_ENV_DATABASE_URLuses internal service name
topologySpreadConstraints blocking pod scheduling (dev) Dev cluster has a single node. Remove the constraint with a JSON6902 patch in kustomization.yaml:
- target: { kind: Deployment, name: shell-api }
patch: |-
- op: remove
path: /spec/template/spec/topologySpreadConstraintsStrategic merge patch with [] is a no-op in kustomize — use JSON6902 op: remove.
PKCE code verifier not specified The code_verifier was not sent in the token exchange. Fixed — see authentication troubleshooting.
Docs site shows 404 on deep links nginx must serve index.html for all routes. Add to your nginx config:
location / {
try_files $uri $uri/ $uri.html /index.html;
}Shell UI loads but apps show blank screen Module Federation remote URL mismatch. Check VITE_API_URL and the Service Registry entry for each remote — the remoteEntry.js URL must be reachable from the browser.