This template supports deployment to OpenShift/Kubernetes using:
- Docker/Podman for container builds
- Quay.io for container registry
- Kustomize for environment configuration
- OpenShift Routes for ingress
# Build containers
make build # Build with 'latest' tag
make build TAG=v1.0.0 # Build with specific tag
make build-prod # Build with 'prod' tag
# Push to registry
make push # Push with 'latest' tag
make push TAG=v1.0.0 # Push with specific tag
make push-prod # Push with 'prod' tag
# Deploy
make deploy # Deploy to dev environment
make deploy-prod # Deploy to prod environment
make undeploy # Remove dev deployment
make undeploy-prod # Remove prod deployment
# Database in cluster
make db-init-cluster # Run migrations + seed data
make db-migrate-cluster # Run migrations only
make db-seed-cluster # Run seed data only# Default values (can be overridden)
REGISTRY ?= quay.io/cfchase
TAG ?= latest
CONTAINER_TOOL ?= docker # or podman# Build both frontend and backend
make build
# With custom registry and tag
make build REGISTRY=my-registry.io/myorg TAG=v1.0.0
# Using podman
make build CONTAINER_TOOL=podman- Frontend:
${REGISTRY}/frontend:${TAG} - Backend:
${REGISTRY}/backend:${TAG}
- Create account at quay.io
- Create repositories:
frontend,backend - Configure robot account or login:
docker login quay.io # or podman login quay.io
# Push both images
make push
# Push with specific tag
make push TAG=v1.0.0
# Production push
make push-prod # Uses TAG=prodk8s/
├── app/ # Deep Research app (Kustomize)
│ ├── base/
│ │ ├── kustomization.yaml
│ │ ├── deployment.yaml # Combined pod (frontend+backend+oauth-proxy)
│ │ ├── service.yaml
│ │ ├── route.yaml
│ │ ├── serviceaccount.yaml
│ │ └── oauth2-proxy-config.yaml
│ └── overlays/
│ ├── dev/
│ │ ├── kustomization.yaml
│ │ ├── oauth-proxy.env
│ │ └── oauth-proxy-secret.env # gitignored
│ └── prod/
├── postgres/ # PostgreSQL database (Kustomize)
│ ├── base/
│ └── overlays/dev/
├── langflow/ # Legacy Kustomize (reference only)
│ └── README.md # Points to Helm deployment
├── mlflow/ # Legacy Kustomize (reference only)
│ └── README.md # Points to Helm deployment
│
helm/
├── langfuse/ # Langfuse Helm values
│ ├── values-dev.yaml
│ └── secrets-dev.yaml # Auto-generated (gitignored)
├── langflow/ # LangFlow Helm values
│ └── values-dev.yaml
└── mlflow/ # MLFlow Helm values
└── values-dev.yaml
│
scripts/
├── deploy.sh # Full deployment orchestrator
├── deploy-db.sh # PostgreSQL deployment
├── deploy-app.sh # App deployment
├── deploy-langflow.sh # LangFlow Helm deployment
├── deploy-mlflow.sh # MLFlow Helm deployment
├── deploy-langfuse.sh # Langfuse Helm deployment
└── undeploy.sh # Full cleanup
The application uses a consolidated pod deployment with multiple containers:
┌─────────────────────┐
│ OpenShift Route │
│ (External Access) │
└──────────┬──────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ App Pod │
│ ┌────────────────┐ │
│ │ OAuth2 Proxy │◄── All external requests enter here │
│ │ (Port 4180) │ │
│ │ │ - Authenticates users │
│ │ ENTRY POINT │ - Sets X-Forwarded-User headers │
│ └───────┬────────┘ - Redirects to OAuth provider │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Frontend │ - Serves React static files │
│ │ (Port 8080) │ - Proxies /api/* to backend │
│ │ │ │
│ │ Nginx Proxy │ │
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Backend │ - FastAPI application │
│ │ (Port 8000) │ - GraphQL + REST APIs │
│ │ │ - Admin panel │
│ │ INTERNAL ONLY │◄── Cluster-internal, NOT directly exposed │
│ └────────────────┘ │
│ │
│ Init Container: db-migration (runs alembic upgrade head) │
└──────────────────────────────────────────────────────────────────┘
Security Architecture:
- Backend is INTERNAL ONLY: Not directly accessible from outside the cluster
- All requests flow through OAuth2 Proxy: Authentication is enforced
- Frontend proxies API calls: Backend only receives authenticated requests
- X-Forwarded-User headers: Set by OAuth2 Proxy, trusted by backend
Key Features:
- Init Container: Runs database migrations before app starts
- OAuth2 Proxy Sidecar: Handles authentication
- Security Contexts: runAsNonRoot, dropped capabilities
- Resource Limits: Defined for all containers
Development (k8s/overlays/dev/):
- Uses
latestimage tag - Includes in-cluster PostgreSQL deployment
- Lower resource limits
- OAuth2 proxy configured for dev
Production (k8s/overlays/prod/):
- Uses
prodimage tag - Uses external/managed database
- Higher resource limits
- Production OAuth2 secrets
CRITICAL: Before deploying, you must create the OAuth2 proxy secret file.
-
Copy the example file:
# For development cp k8s/overlays/dev/oauth-proxy-secret.env.example k8s/overlays/dev/oauth-proxy-secret.env # For production cp k8s/overlays/prod/oauth-proxy-secret.env.example k8s/overlays/prod/oauth-proxy-secret.env
-
Generate a cookie secret:
openssl rand -base64 32 | tr -- '+/' '-_'
-
Edit the secret file with your OAuth provider credentials:
# oauth-proxy-secret.env client-id=your-oauth-client-id client-secret=your-oauth-client-secret cookie-secret=<generated-cookie-secret>
-
The secret file is gitignored - never commit OAuth secrets!
See AUTHENTICATION.md for OAuth provider configuration details.
# Preview manifests
make kustomize # Dev environment
make kustomize-prod # Prod environment
# Apply to cluster
make deploy # Dev environment
make deploy-prod # Prod environment
# Remove deployment
make undeploy
make undeploy-prodThe deployment includes three AI/ML services, all deployed via Helm:
| Service | Purpose | Authentication | Helm Chart |
|---|---|---|---|
| LangFlow | Visual workflow builder | Shared admin credentials | langflow/langflow-ide |
| Langfuse | LLM observability | Built-in email/password | langfuse/langfuse |
| MLFlow | Experiment tracking | Shared admin credentials (HTTP Basic) | community-charts/mlflow |
All services are deployed automatically with make deploy:
make deployAt the end of deployment, credentials and URLs are displayed:
========================================
ADMIN CREDENTIALS
========================================
Email: admin@localhost.local
Password: <auto-generated>
SERVICE URLS
========================================
Deep Research: https://multi-agent-platform-multi-agent-platform-dev.<cluster-domain>
LangFlow: https://langflow-multi-agent-platform-dev.<cluster-domain>
Langfuse: https://langfuse-multi-agent-platform-dev.<cluster-domain>
MLFlow: https://mlflow-multi-agent-platform-dev.<cluster-domain>
========================================
# Show credentials and URLs anytime
make get-admin-credentialsAll AI/ML services are deployed via Helm and share the PostgreSQL database.
LangFlow (langflow/langflow-ide)
- StatefulSet deployment with frontend + backend
- Uses shared PostgreSQL for metadata
- Admin credentials from
admin-credentialssecret
MLFlow (community-charts/mlflow)
- Deployment with PostgreSQL backend store
- HTTP Basic authentication enabled
- Admin credentials from
admin-credentialssecret
Langfuse (langfuse/langfuse)
- Includes Redis, ClickHouse, Zookeeper subcharts
- Uses shared PostgreSQL for application data
- Built-in authentication (email/password signup)
- Secrets auto-generated on first deploy
# Deploy individual components
make deploy-db # PostgreSQL only
make deploy-app # Deep Research app only
make deploy-langflow # LangFlow only
make deploy-mlflow # MLFlow only
make deploy-langfuse # Langfuse only# Langfuse management
make helm-langfuse-status # Check status
make helm-langfuse-upgrade # Upgrade release
make helm-langfuse-logs # View logs
make helm-langfuse-uninstall # Remove
# LangFlow management
make helm-langflow-status # Check status
make helm-langflow-logs # View logs
# MLFlow management
make helm-mlflow-status # Check status
make helm-mlflow-logs # View logsAfter deploying, initialize the database:
# Option 1: Migrations + seed data (recommended for dev)
make db-init-cluster
# Option 2: Migrations only (for production)
make db-migrate-cluster
# Option 3: Seed data only (after migrations)
make db-seed-cluster# Delete existing jobs first
oc delete job db-migration db-seed
# Then re-run
make db-init-clusterFor production, consider using a managed database:
-
Create managed PostgreSQL instance
-
Update secret in overlay:
# k8s/overlays/prod/postgres-secret.yaml apiVersion: v1 kind: Secret metadata: name: postgres-secret stringData: username: produser password: securepassword database: proddb host: managed-postgres.example.com port: "5432"
-
Remove PostgreSQL deployment from prod overlay
The base kustomization includes a Route resource:
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: frontend-route
spec:
to:
kind: Service
name: frontend
port:
targetPort: 8080
tls:
termination: edge# In overlay kustomization
patches:
- target:
kind: Route
name: frontend-route
patch: |
- op: add
path: /spec/host
value: myapp.example.comRequired environment variables:
env:
- name: POSTGRES_SERVER
valueFrom:
secretKeyRef:
name: postgres-secret
key: host
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secret
key: databaseAdd to overlay:
# k8s/overlays/prod/kustomization.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=INFO
- FEATURE_FLAG=enabled
patches:
- target:
kind: Deployment
name: backend
patch: |
- op: add
path: /spec/template/spec/containers/0/envFrom/-
value:
configMapRef:
name: app-configThe backend includes a health endpoint:
GET /api/v1/utils/health-check
Kubernetes probes:
livenessProbe:
httpGet:
path: /api/v1/utils/health-check
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/utils/health-check
port: 8000
initialDelaySeconds: 5
periodSeconds: 5# Check pod status
oc get pods
# Check pod logs
oc logs <pod-name>
# Check events
oc get events --sort-by='.lastTimestamp'
# Describe pod
oc describe pod <pod-name># Verify postgres is running
oc get pods -l app=postgres
# Check postgres logs
oc logs -l app=postgres
# Verify secret
oc get secret postgres-secret -o yaml# Check image pull secret
oc get secrets | grep pull
# Create pull secret if needed
oc create secret docker-registry quay-pull \
--docker-server=quay.io \
--docker-username=<user> \
--docker-password=<password>name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Quay
run: docker login quay.io -u ${{ secrets.QUAY_USER }} -p ${{ secrets.QUAY_TOKEN }}
- name: Build and Push
run: |
make build TAG=${{ github.sha }}
make push TAG=${{ github.sha }}
- name: Deploy
run: |
# Update image tag in overlay
# Apply to cluster- DEVELOPMENT.md - Local development
- DATABASE.md - Database configuration
- ../CLAUDE.md - Project overview